IDEA-170885 Simplify action for Optional

This commit is contained in:
Tagir Valeev
2017-04-27 18:02:23 +07:00
parent 0b960ec6ad
commit 52e7fdf66f
21 changed files with 467 additions and 12 deletions
@@ -0,0 +1,157 @@
/*
* Copyright 2000-2017 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.codeInspection.util.OptionalUtil;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiDiamondTypeUtil;
import com.intellij.psi.util.PsiExpressionTrimRenderer;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.util.LambdaRefactoringUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.MethodCallUtils;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* @author Tagir Valeev
*/
public class SimplifyOptionalCallChainsInspection extends BaseJavaBatchLocalInspectionTool {
private static final CallMatcher OPTIONAL_OR_ELSE =
CallMatcher.instanceCall(CommonClassNames.JAVA_UTIL_OPTIONAL, "orElse").parameterCount(1);
private static final CallMatcher OPTIONAL_OR_ELSE_GET =
CallMatcher.instanceCall(CommonClassNames.JAVA_UTIL_OPTIONAL, "orElseGet").parameterCount(1);
private static final CallMatcher OPTIONAL_MAP =
CallMatcher.instanceCall(CommonClassNames.JAVA_UTIL_OPTIONAL, "map").parameterCount(1);
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) {
return PsiElementVisitor.EMPTY_VISITOR;
}
return new JavaElementVisitor() {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression call) {
PsiExpression falseArg = null;
boolean useOrElseGet = false;
if (OPTIONAL_OR_ELSE.test(call)) {
falseArg = call.getArgumentList().getExpressions()[0];
}
else if (OPTIONAL_OR_ELSE_GET.test(call)) {
useOrElseGet = true;
PsiLambdaExpression lambda = getLambda(call.getArgumentList().getExpressions()[0]);
if (lambda == null || lambda.getParameterList().getParametersCount() != 0) return;
falseArg = LambdaUtil.extractSingleExpressionFromBody(lambda.getBody());
}
if (falseArg == null) return;
PsiMethodCallExpression qualifierCall = MethodCallUtils.getQualifierMethodCall(call);
if (!OPTIONAL_MAP.test(qualifierCall)) return;
PsiLambdaExpression lambda = getLambda(qualifierCall.getArgumentList().getExpressions()[0]);
if (lambda == null) return;
PsiExpression trueArg = LambdaUtil.extractSingleExpressionFromBody(lambda.getBody());
if (trueArg == null) return;
PsiParameter[] parameters = lambda.getParameterList().getParameters();
if (parameters.length != 1) return;
PsiExpression qualifier = qualifierCall.getMethodExpression().getQualifierExpression();
if (qualifier == null) return;
String opt = qualifier.getText();
PsiParameter parameter = parameters[0];
String proposed = OptionalUtil.generateOptionalUnwrap(opt, parameter, trueArg, falseArg, call.getType(), useOrElseGet);
String canonicalOrElse;
if (useOrElseGet && !ExpressionUtils.isSimpleExpression(falseArg)) {
canonicalOrElse = ".orElseGet(() -> " + falseArg.getText() + ")";
}
else {
canonicalOrElse = ".orElse(" + falseArg.getText() + ")";
}
String canonical = opt + ".map(" + LambdaUtil.createLambda(parameter, trueArg) + ")" + canonicalOrElse;
if (proposed.length() < canonical.length()) {
String displayCode;
if(proposed.equals(opt)) {
displayCode = "";
} else if(opt.length() > 10) {
// should be a parseable expression
opt = "(($))";
String template = OptionalUtil.generateOptionalUnwrap(opt, parameter, trueArg, falseArg, call.getType(), useOrElseGet);
displayCode =
PsiExpressionTrimRenderer.render(JavaPsiFacade.getElementFactory(holder.getProject()).createExpressionFromText(template, call));
displayCode = displayCode.replaceFirst(Pattern.quote(opt), "..");
} else {
displayCode =
PsiExpressionTrimRenderer.render(JavaPsiFacade.getElementFactory(holder.getProject()).createExpressionFromText(proposed, call));
}
holder.registerProblem(Objects.requireNonNull(call.getMethodExpression().getReferenceNameElement()),
"Optional chain can be simplified",
new SimplifyOptionalChainFix(proposed, displayCode));
}
}
};
}
private static PsiLambdaExpression getLambda(PsiExpression initializer) {
PsiExpression expression = PsiUtil.skipParenthesizedExprDown(initializer);
if (expression instanceof PsiLambdaExpression) {
return (PsiLambdaExpression)expression;
}
if (expression instanceof PsiMethodReferenceExpression) {
return LambdaRefactoringUtil.createLambda((PsiMethodReferenceExpression)expression, true);
}
return null;
}
private static class SimplifyOptionalChainFix implements LocalQuickFix {
private final String myReplacement;
private final String myDisplayCode;
public SimplifyOptionalChainFix(String replacement, String displayCode) {
myReplacement = replacement;
myDisplayCode = displayCode;
}
@Nls
@NotNull
@Override
public String getName() {
return myDisplayCode.isEmpty() ? "Remove redundant steps from optional chain" :
"Simplify optional chain to '"+myDisplayCode+"'";
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return "Simplify optional call chain";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(descriptor.getStartElement(), PsiMethodCallExpression.class);
if (call == null) return;
PsiExpression replacementExpression = JavaPsiFacade.getElementFactory(project).createExpressionFromText(myReplacement, call);
PsiElement result = call.replace(replacementExpression);
LambdaCanBeMethodReferenceInspection.replaceAllLambdasWithMethodReferences(result);
PsiDiamondTypeUtil.removeRedundantTypeArguments(result);
}
}
}
@@ -17,9 +17,12 @@ package com.intellij.codeInspection.util;
import com.intellij.codeInsight.PsiEquivalenceUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.refactoring.util.RefactoringUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.BoolUtils;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.MethodCallUtils;
import org.jetbrains.annotations.Contract;
@@ -33,6 +36,9 @@ public class OptionalUtil {
private static final String OPTIONAL_LONG = "java.util.OptionalLong";
private static final String OPTIONAL_DOUBLE = "java.util.OptionalDouble";
private static final CallMatcher OPTIONAL_OF =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_OPTIONAL, "of", "ofNullable").parameterCount(1);
@NotNull
@Contract(pure = true)
public static String getOptionalClass(String type) {
@@ -102,37 +108,59 @@ public class OptionalUtil {
PsiExpression trueExpression, PsiExpression falseExpression,
PsiType targetType, boolean useOrElseGet) {
if (!ExpressionUtils.isReferenceTo(trueExpression, var)) {
if(trueExpression instanceof PsiTypeCastExpression && ExpressionUtils.isNullLiteral(falseExpression)) {
if (trueExpression instanceof PsiTypeCastExpression && ExpressionUtils.isNullLiteral(falseExpression)) {
PsiTypeCastExpression castExpression = (PsiTypeCastExpression)trueExpression;
PsiTypeElement castType = castExpression.getCastType();
// pull cast outside to avoid the .map() step
if(castType != null && ExpressionUtils.isReferenceTo(castExpression.getOperand(), var)) {
if (castType != null && ExpressionUtils.isReferenceTo(castExpression.getOperand(), var)) {
return "(" + castType.getText() + ")" + qualifier + ".orElse(null)";
}
}
if(ExpressionUtils.isLiteral(falseExpression, Boolean.FALSE) && PsiType.BOOLEAN.equals(trueExpression.getType())) {
if (ExpressionUtils.isLiteral(falseExpression, Boolean.FALSE) && PsiType.BOOLEAN.equals(trueExpression.getType())) {
if (ExpressionUtils.isLiteral(trueExpression, Boolean.TRUE)) {
return qualifier + ".isPresent()";
}
return qualifier + ".filter(" + LambdaUtil.createLambda(var, trueExpression) + ").isPresent()";
}
if(trueExpression instanceof PsiConditionalExpression) {
if (ExpressionUtils.isLiteral(falseExpression, Boolean.TRUE) && ExpressionUtils.isLiteral(trueExpression, Boolean.FALSE)) {
return "!" + qualifier + ".isPresent()";
}
if (trueExpression instanceof PsiConditionalExpression) {
PsiConditionalExpression condition = (PsiConditionalExpression)trueExpression;
PsiExpression thenExpression = condition.getThenExpression();
PsiExpression elseExpression = condition.getElseExpression();
if(elseExpression != null && PsiEquivalenceUtil.areElementsEquivalent(falseExpression, elseExpression)) {
if (elseExpression != null && PsiEquivalenceUtil.areElementsEquivalent(falseExpression, elseExpression)) {
return generateOptionalUnwrap(
qualifier + ".filter(" + LambdaUtil.createLambda(var, condition.getCondition()) + ")", var,
condition.getThenExpression(), falseExpression, targetType, useOrElseGet);
}
if (thenExpression != null && PsiEquivalenceUtil.areElementsEquivalent(falseExpression, thenExpression)) {
return generateOptionalUnwrap(
qualifier + ".filter(" + var.getName() + " -> " + BoolUtils.getNegatedExpressionText(condition.getCondition()) + ")", var,
condition.getElseExpression(), falseExpression, targetType, useOrElseGet);
}
}
if(isOptionalEmptyCall(falseExpression)) {
String suffix = null;
if (isOptionalEmptyCall(falseExpression)) {
suffix = "";
}
else if (PsiUtil.isLanguageLevel9OrHigher(trueExpression) &&
InheritanceUtil.isInheritor(falseExpression.getType(), CommonClassNames.JAVA_UTIL_OPTIONAL) &&
LambdaGenerationUtil.canBeUncheckedLambda(falseExpression)) {
suffix = ".or(() -> " + falseExpression.getText() + ")";
}
if (suffix != null) {
// simplify "qualifier.map(x -> Optional.of(x)).orElse(Optional.empty())" to "qualifier"
if (trueExpression instanceof PsiMethodCallExpression &&
MethodCallUtils.isCallToStaticMethod((PsiMethodCallExpression)trueExpression, CommonClassNames.JAVA_UTIL_OPTIONAL, "of", 1)) {
if (trueExpression instanceof PsiMethodCallExpression && OPTIONAL_OF.test((PsiMethodCallExpression)trueExpression)) {
PsiExpression arg = ((PsiMethodCallExpression)trueExpression).getArgumentList().getExpressions()[0];
if(ExpressionUtils.isReferenceTo(arg, var)) {
return qualifier;
if (ExpressionUtils.isReferenceTo(arg, var)) {
return qualifier + suffix;
}
return qualifier + ".map(" + LambdaUtil.createLambda(var, arg) + ")";
return qualifier + ".map(" + LambdaUtil.createLambda(var, arg) + ")" + suffix;
}
if (suffix.isEmpty()) {
return qualifier + ".flatMap(" + LambdaUtil.createLambda(var, trueExpression) + ")";
}
return qualifier + ".flatMap(" + LambdaUtil.createLambda(var, trueExpression) + ")";
}
trueExpression =
targetType == null ? trueExpression : RefactoringUtil.convertInitializerToNormalExpression(trueExpression, targetType);
@@ -0,0 +1,8 @@
// "Simplify optional chain to '(String)...orElse(...)'" "true"
import java.util.Optional;
public class Test {
public void test(Optional<Object> opt) {
String result = (String) opt.filter(opt -> opt instanceof String).orElse(null);
}
}
@@ -0,0 +1,12 @@
// "Simplify optional chain to 'opt.filter(...).map(...).orElseGet(...)'" "true"
import java.util.Optional;
public class Test {
public String getDefaultValue() {
return "foo";
}
public void test(Optional<String> opt) {
String result = opt.filter(obj -> !obj.isEmpty()).map(String::trim).orElseGet(this::getDefaultValue);
}
}
@@ -0,0 +1,12 @@
// "Simplify optional chain to '!...isPresent()'" "true"
import java.util.Optional;
public class Test {
interface Dto {
int getId();
}
public void test(Dto dto) {
boolean present = !Optional.ofNullable(dto).map(Dto::getId).isPresent();
}
}
@@ -0,0 +1,12 @@
// "Remove redundant steps from optional chain" "true"
import java.util.Optional;
public class Test {
public Optional<String> test() {
return getOpt();
}
private Optional<String> getOpt() {
return Optional.of("foo");
}
}
@@ -0,0 +1,12 @@
// "Simplify optional chain to 'getOpt().map(...)'" "true"
import java.util.Optional;
public class Test {
public Optional<String> test() {
return getOpt().map(String::trim);
}
private Optional<String> getOpt() {
return Optional.of("foo");
}
}
@@ -0,0 +1,16 @@
// "Simplify optional chain to 'getOpt().or(...)'" "true"
import java.util.Optional;
public class Test {
public Optional<String> test() {
return getOpt().or(this::getOpt2);
}
private Optional<String> getOpt() {
return Optional.of("foo");
}
private Optional<String> getOpt2() {
return Optional.of("bar");
}
}
@@ -0,0 +1,12 @@
// "Simplify optional chain to '...isPresent()'" "true"
import java.util.Optional;
public class Test {
interface Dto {
int getId();
}
public void test(Dto dto) {
boolean present = Optional.ofNullable(dto).map(Dto::getId).isPresent();
}
}
@@ -0,0 +1,8 @@
// "Simplify optional chain to '(String)...orElse(...)'" "true"
import java.util.Optional;
public class Test {
public void test(Optional<Object> opt) {
String result = opt.filter(opt -> opt instanceof String).map(opt -> (String) opt).or<caret>Else(null);
}
}
@@ -0,0 +1,12 @@
// "Simplify optional chain to 'opt.filter(...).map(...).orElseGet(...)'" "true"
import java.util.Optional;
public class Test {
public String getDefaultValue() {
return "foo";
}
public void test(Optional<String> opt) {
String result = opt.map(obj -> obj.isEmpty() ? getDefaultValue() : obj.trim()).orEl<caret>seGet(this::getDefaultValue);
}
}
@@ -0,0 +1,14 @@
// "Fix all 'Simplify Optional call chains' problems in file" "false"
import java.util.Optional;
public class Test {
public String getDefaultValue() {
return "foo";
}
public void test(Optional<String> opt) {
// proposed change is longer:
// opt.filter(obj -> !obj.isEmpty()).map(obj -> obj.trim()).orElse(null);
String result = opt.map(obj -> obj.isEmpty() ? null : obj.trim()).orEl<caret>se(null);
}
}
@@ -0,0 +1,14 @@
// "Simplify optional chain to '!...isPresent()'" "true"
import java.util.Optional;
public class Test {
interface Dto {
int getId();
}
public void test(Dto dto) {
boolean present = Optional.ofNullable(dto).map(Dto::getId).map(obj -> {
return false;
}).or<caret>ElseGet(() -> true);
}
}
@@ -0,0 +1,12 @@
// "Remove redundant steps from optional chain" "true"
import java.util.Optional;
public class Test {
public Optional<String> test() {
return getOpt().map(Optional::of).orEl<caret>se(Optional.empty());
}
private Optional<String> getOpt() {
return Optional.of("foo");
}
}
@@ -0,0 +1,12 @@
// "Simplify optional chain to 'getOpt().map(...)'" "true"
import java.util.Optional;
public class Test {
public Optional<String> test() {
return getOpt().map(x -> Optional.ofNullable(x.trim())).orEl<caret>seGet(Optional::empty);
}
private Optional<String> getOpt() {
return Optional.of("foo");
}
}
@@ -0,0 +1,17 @@
// "Simplify optional chain to 'getOpt().or(...)'" "false"
import java.util.Optional;
public class Test {
public Optional<String> test() {
// no simpler alternative in Java 8
return getOpt().map(Optional::of).orEl<caret>seGet(this::getOpt2);
}
private Optional<String> getOpt() {
return Optional.of("foo");
}
private Optional<String> getOpt2() {
return Optional.of("bar");
}
}
@@ -0,0 +1,16 @@
// "Simplify optional chain to 'getOpt().or(...)'" "true"
import java.util.Optional;
public class Test {
public Optional<String> test() {
return getOpt().map(Optional::of).orEl<caret>seGet(this::getOpt2);
}
private Optional<String> getOpt() {
return Optional.of("foo");
}
private Optional<String> getOpt2() {
return Optional.of("bar");
}
}
@@ -0,0 +1,12 @@
// "Simplify optional chain to '...isPresent()'" "true"
import java.util.Optional;
public class Test {
interface Dto {
int getId();
}
public void test(Dto dto) {
boolean present = Optional.ofNullable(dto).map(Dto::getId).map(obj -> true).or<caret>Else(false);
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2000-2017 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.openapi.projectRoots.Sdk;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.testFramework.IdeaTestUtil;
import org.jetbrains.annotations.NotNull;
/**
* @author Tagir Valeev
*/
public class SimplifyOptionalCallChainsInspectionTest extends LightQuickFixParameterizedTestCase {
@Override
protected LanguageLevel getLanguageLevel() {
return getTestName(false).endsWith("Java9.java") ? LanguageLevel.JDK_1_9 : LanguageLevel.JDK_1_8;
}
@Override
protected Sdk getProjectJDK() {
return getTestName(false).endsWith("Java9.java") ? IdeaTestUtil.getMockJdk9() : IdeaTestUtil.getMockJdk18();
}
@NotNull
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new SimplifyOptionalCallChainsInspection()};
}
public void test() throws Exception {
doAllTests();
}
@Override
protected String getBasePath() {
return "/inspection/optionalChains";
}
}
@@ -0,0 +1,12 @@
<html>
<body>
<p>This inspection warns about Optional call chains which could be simplified. Here are some examples of possible simplifications:</p>
<ul>
<li><code>optional.map(x -> true).orElse(false)</code> &rarr; <code>optional.isPresent()</code></li>
<li><code>optional.map(x -> Optional.of(x.trim())).orElseGet(Optional::empty)</code> &rarr; <code>optional.map(String::trim)</code></li>
<li><code>optional.map(x -> (String)x).orElse(null)</code> &rarr; <code>(String) optional.orElse(null)</code></li>
</ul>
<!-- tooltip end -->
<p><small>New in 2017.2</small></p>
</body>
</html>
+5
View File
@@ -889,6 +889,11 @@
groupKey="group.names.declaration.redundancy" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.SimplifyStreamApiCallChainsInspection"
displayName="Simplify stream API call chains"/>
<localInspection groupPath="Java" language="JAVA" shortName="SimplifyOptionalCallChains"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.declaration.redundancy" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.SimplifyOptionalCallChainsInspection"
displayName="Simplify Optional call chains"/>
<localInspection groupPath="Java" language="JAVA" shortName="SimplifyCollector"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.declaration.redundancy" enabledByDefault="true" level="WARNING"