Inspection: Expression can be folded into Stream chain (IDEA-187123)

This commit is contained in:
Tagir Valeev
2018-03-05 16:33:08 +07:00
parent 874011da51
commit 2c4af2781d
23 changed files with 382 additions and 32 deletions

View File

@@ -758,6 +758,11 @@
enabledByDefault="true" level="WARNING"
key="inspection.redundant.comparator.comparing.display.name" bundle="messages.InspectionsBundle"
implementationClass="com.intellij.codeInspection.RedundantComparatorComparingInspection"/>
<localInspection groupPath="Java,Java language level migration aids" language="JAVA" shortName="FoldExpressionIntoStream"
groupBundle="messages.InspectionsBundle" groupKey="group.names.language.level.specific.issues.and.migration.aids8"
enabledByDefault="true" level="INFORMATION"
key="inspection.fold.expression.into.stream.display.name" bundle="messages.InspectionsBundle"
implementationClass="com.intellij.codeInspection.streamMigration.FoldExpressionIntoStreamInspection"/>
<globalInspection groupPath="Java" language="JAVA" shortName="EmptyMethod" displayName="Empty method" groupKey="group.names.declaration.redundancy" enabledByDefault="true" groupBundle="messages.InspectionsBundle"
level="WARNING" implementationClass="com.intellij.codeInspection.emptyMethod.EmptyMethodInspection"/>
<globalInspection groupPath="Java" language="JAVA" shortName="UnusedReturnValue" bundle="messages.InspectionsBundle" key="inspection.unused.return.value.display.name"

View File

@@ -0,0 +1,207 @@
// 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.streamMigration;
import com.intellij.codeInsight.PsiEquivalenceUtil;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.util.LambdaGenerationUtil;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.SuggestedNameInfo;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.impl.PsiDiamondTypeUtil;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.ig.psiutils.*;
import one.util.streamex.IntStreamEx;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static com.intellij.codeInsight.intention.impl.StreamRefactoringUtil.getMapOperationName;
import static com.intellij.util.ObjectUtils.tryCast;
public class FoldExpressionIntoStreamInspection extends AbstractBaseJavaLocalInspectionTool {
@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 visitPolyadicExpression(PsiPolyadicExpression expression) {
TerminalGenerator generator = getGenerator(expression);
if (generator == null) return;
if (extractDiff(generator, expression).isEmpty()) return;
if (!LambdaGenerationUtil.canBeUncheckedLambda(expression)) return;
holder.registerProblem(expression, InspectionsBundle.message("inspection.fold.expression.into.stream.display.name"), new FoldExpressionIntoStreamFix());
}
};
}
private static List<PsiExpression> extractDiff(TerminalGenerator generator,
PsiPolyadicExpression expression) {
EquivalenceChecker equivalence = EquivalenceChecker.getCanonicalPsiEquivalence();
PsiExpression[] operands = generator.getOperands(expression);
if (operands.length < 3) return Collections.emptyList();
List<PsiExpression> elements = new ArrayList<>();
for (int i = 1; i < operands.length; i++) {
if (!Objects.equals(operands[0].getType(), operands[i].getType())) return Collections.emptyList();
EquivalenceChecker.Match match = equivalence.expressionsMatch(operands[0], operands[i]);
if (!match.isPartialMatch()) return Collections.emptyList();
PsiExpression left = tryCast(match.getLeftDiff(), PsiExpression.class);
PsiExpression right = tryCast(match.getRightDiff(), PsiExpression.class);
if (left == null || right == null) return Collections.emptyList();
if (elements.isEmpty()) {
if (!StreamApiUtil.isSupportedStreamElement(left.getType()) || !ExpressionUtils.isSafelyRecomputableExpression(left)) {
return Collections.emptyList();
}
if (operands[0] instanceof PsiBinaryExpression) {
PsiBinaryExpression binOp = (PsiBinaryExpression)operands[0];
if (ComparisonUtils.isComparison(binOp) &&
(left == binOp.getLOperand() && ExpressionUtils.isSafelyRecomputableExpression(binOp.getROperand())) ||
(left == binOp.getROperand() && ExpressionUtils.isSafelyRecomputableExpression(binOp.getLOperand()))) {
// Disable for simple comparison chains like "a == null && b == null && c == null":
// using Stream API here looks an overkill
return Collections.emptyList();
}
}
elements.add(left);
}
else if (elements.get(0) != left) {
return Collections.emptyList();
}
if (!Objects.equals(left.getType(), right.getType()) ||
!ExpressionUtils.isSafelyRecomputableExpression(right)) {
return Collections.emptyList();
}
elements.add(right);
}
return elements;
}
private interface TerminalGenerator {
default PsiExpression[] getOperands(PsiPolyadicExpression polyadicExpression) {
return polyadicExpression.getOperands();
}
@NotNull
String generateTerminal(PsiType elementType, String lambda, CommentTracker ct);
}
@Nullable
private static TerminalGenerator getGenerator(PsiPolyadicExpression polyadicExpression) {
IElementType tokenType = polyadicExpression.getOperationTokenType();
if (tokenType.equals(JavaTokenType.OROR)) {
return (elementType, lambda, ct) -> ".anyMatch(" + lambda + ")";
}
else if (tokenType.equals(JavaTokenType.ANDAND)) {
return (elementType, lambda, ct) -> ".allMatch(" + lambda + ")";
}
else if (tokenType.equals(JavaTokenType.PLUS)) {
PsiType type = polyadicExpression.getType();
if (type instanceof PsiPrimitiveType) {
if (!StreamApiUtil.isSupportedStreamElement(type)) return null;
return (elementType, lambda, ct) -> "." + getMapOperationName(elementType, type) + "(" + lambda + ").sum()";
}
if (!TypeUtils.isJavaLangString(type)) return null;
PsiExpression[] operands = polyadicExpression.getOperands();
String mapToString;
PsiType operandType = operands[0].getType();
if (!InheritanceUtil.isInheritor(operandType, "java.lang.CharSequence")) {
if (!StreamApiUtil.isSupportedStreamElement(operandType)) return null;
mapToString = "."+getMapOperationName(operandType, type)+"(String::valueOf)";
} else {
mapToString = "";
}
if (operands.length > 4 && operands.length % 2 == 1 && ExpressionUtils.isSafelyRecomputableExpression(operands[1]) &&
IntStreamEx.range(1, operands.length, 2).elements(operands).pairMap(PsiEquivalenceUtil::areElementsEquivalent)
.allMatch(Boolean.TRUE::equals)) {
PsiExpression delimiter = operands[1];
return new TerminalGenerator() {
@Override
public PsiExpression[] getOperands(PsiPolyadicExpression polyadicExpression) {
PsiExpression[] ops = polyadicExpression.getOperands();
return IntStreamEx.range(0, ops.length, 2).elements(ops).toArray(PsiExpression.EMPTY_ARRAY);
}
@NotNull
@Override
public String generateTerminal(PsiType elementType, String lambda, CommentTracker ct) {
return mapToString(elementType, operandType, lambda) + mapToString +
".collect(" + CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS + ".joining(" + ct.text(delimiter) + "))";
}
};
}
return (elementType, lambda, ct) -> mapToString(elementType, operandType, lambda) + mapToString +
".collect(" + CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS + ".joining())";
}
return null;
}
@NotNull
private static String mapToString(PsiType elementType, PsiType resultType, String lambda) {
return "." + getMapOperationName(elementType, resultType) + "(" + lambda + ")";
}
private static class FoldExpressionIntoStreamFix implements LocalQuickFix {
@Nls
@NotNull
@Override
public String getFamilyName() {
return InspectionsBundle.message("inspection.fold.expression.into.stream.fix.name");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiPolyadicExpression expression = tryCast(descriptor.getStartElement(), PsiPolyadicExpression.class);
if (expression == null) return;
TerminalGenerator generator = getGenerator(expression);
if (generator == null) return;
List<PsiExpression> diffs = extractDiff(generator, expression);
if (diffs.isEmpty()) return;
PsiExpression[] operands = expression.getOperands();
PsiExpression firstExpression = diffs.get(0);
assert PsiTreeUtil.isAncestor(operands[0], firstExpression, true);
Object marker = new Object();
PsiTreeUtil.mark(firstExpression, marker);
CommentTracker ct = new CommentTracker();
PsiExpression operandCopy = (PsiExpression)ct.markUnchanged(operands[0]).copy();
PsiElement expressionCopy = PsiTreeUtil.releaseMark(operandCopy, marker);
if (expressionCopy == null) return;
JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(project);
PsiType elementType = firstExpression.getType();
SuggestedNameInfo info = codeStyleManager.suggestVariableName(VariableKind.PARAMETER, null, null, elementType, true);
String name = info.names.length > 0 ? info.names[0] : "v";
name = codeStyleManager.suggestUniqueVariableName(name, expression, true);
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
expressionCopy.replace(factory.createExpressionFromText(name, expressionCopy));
String lambda = name + "->" + operandCopy.getText();
String streamClass = StreamApiUtil.getStreamClassForType(elementType);
if (streamClass == null) return;
String source = streamClass + "." + (elementType instanceof PsiClassType ? "<" + elementType.getCanonicalText() + ">" : "")
+ "of" + StreamEx.of(diffs).map(ct::text).joining(",", "(", ")");
String fullStream = source + generator.generateTerminal(elementType, lambda, ct);
PsiElement result = ct.replaceAndRestoreComments(expression, fullStream);
cleanup(result);
}
private static void cleanup(PsiElement result) {
JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(result.getProject());
result = SimplifyStreamApiCallChainsInspection.simplifyStreamExpressions(result);
LambdaCanBeMethodReferenceInspection.replaceAllLambdasWithMethodReferences(result);
result = codeStyleManager.shortenClassReferences(result);
PsiDiamondTypeUtil.removeRedundantTypeArguments(result);
}
}
}

View File

@@ -1091,17 +1091,10 @@ public class StreamApiMigrationInspection extends AbstractBaseJavaLocalInspectio
PsiType type = myExpression.getType();
if (type instanceof PsiArrayType) {
PsiType componentType = ((PsiArrayType)type).getComponentType();
if (componentType.equals(PsiType.INT)) {
return CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM + ".of(" + initializerText + ")";
}
else if (componentType.equals(PsiType.LONG)) {
return CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM + ".of(" + initializerText + ")";
}
else if (componentType.equals(PsiType.DOUBLE)) {
return CommonClassNames.JAVA_UTIL_STREAM_DOUBLE_STREAM + ".of(" + initializerText + ")";
}
else if (componentType instanceof PsiClassType) {
return CommonClassNames.JAVA_UTIL_STREAM_STREAM + ".<" + componentType.getCanonicalText() + ">of(" + initializerText + ")";
String streamClass = StreamApiUtil.getStreamClassForType(componentType);
if (streamClass != null) {
return streamClass + "." + (componentType instanceof PsiClassType ? "<" + componentType.getCanonicalText() + ">" : "")
+ ".of(" + initializerText + ")";
}
}
}
@@ -1307,25 +1300,7 @@ public class StreamApiMigrationInspection extends AbstractBaseJavaLocalInspectio
}
String maybeCondition = myCondition != null ? ct.lambdaText(myVariable, myCondition) + "," : "";
return getStreamClass(myVariable.getType()) + ".iterate(" + ct.text(myInitializer) + "," + maybeCondition + lambda + ")";
}
@Contract(value = "null -> null", pure = true)
private static String getStreamClass(@Nullable PsiType type) {
if (type == null) return null;
if (ClassUtils.isPrimitive(type)) {
if (type.equals(PsiType.INT)) {
return CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM;
}
else if (type.equals(PsiType.DOUBLE)) {
return CommonClassNames.JAVA_UTIL_STREAM_DOUBLE_STREAM;
}
else if (type.equals(PsiType.LONG)) {
return CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM;
}
return null;
}
return CommonClassNames.JAVA_UTIL_STREAM_STREAM;
return StreamApiUtil.getStreamClassForType(myVariable.getType()) + ".iterate(" + ct.text(myInitializer) + "," + maybeCondition + lambda + ")";
}
@Override
@@ -1358,7 +1333,7 @@ public class StreamApiMigrationInspection extends AbstractBaseJavaLocalInspectio
if (initStmt == null || initStmt.getDeclaredElements().length != 1) return null;
PsiLocalVariable variable = tryCast(initStmt.getDeclaredElements()[0], PsiLocalVariable.class);
if (variable == null) return null;
if (getStreamClass(variable.getType()) == null) return null;
if (StreamApiUtil.getStreamClassForType(variable.getType()) == null) return null;
PsiExpression initializer = variable.getInitializer();
if (initializer == null) return null;
PsiStatement update = forStatement.getUpdate();

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>Reports expressions with repeating pattern which could be replaced with Stream API.</p>
<!-- tooltip end -->
<p><small>New in 2018.2</small></p>
</body>
</html>

View File

@@ -0,0 +1,8 @@
import java.util.stream.Stream;
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(String a, String b, String c, String d) {
return Stream.of(a, b, c, d).allMatch(s -> s.startsWith("xyz"));
}
}

View File

@@ -0,0 +1,8 @@
import java.util.stream.IntStream;
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(double[] arr) {
return IntStream.of(1, 3, 7, 9).allMatch(i -> arr[i] >= 5);
}
}

View File

@@ -0,0 +1,8 @@
import java.util.stream.Stream;
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(String a, String b, String c, String d) {
return Stream.of(a, b, c, d).noneMatch(s -> s.startsWith("xyz"));
}
}

View File

@@ -0,0 +1,9 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
// "Fold expression into Stream chain" "true"
class Test {
String foo(String a, String b, String c, String d) {
return Stream.of(a, b, c, d).map(String::trim).collect(Collectors.joining());
}
}

View File

@@ -0,0 +1,9 @@
import java.util.stream.Collectors;
import java.util.stream.IntStream;
// "Fold expression into Stream chain" "true"
class Test {
String foo(int a, int b, int c, int d) {
return IntStream.of(a, b, c, d).map(i -> i * 2).mapToObj(String::valueOf).collect(Collectors.joining("|"));
}
}

View File

@@ -0,0 +1,8 @@
import java.util.stream.Stream;
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(String a, String b, String c, String d) {
return Stream.of(a, b, c, d).noneMatch(s -> s.startsWith("xyz"));
}
}

View File

@@ -0,0 +1,8 @@
import java.util.stream.Stream;
// "Fold expression into Stream chain" "true"
class Test {
int foo(String a, String b, String c, String d) {
return Stream.of(a, b, c, d).mapToInt(String::length).sum();
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(String a, String b, String c, String d) {
return a.startsWith("xyz") &<caret>& b.startsWith("xyz") && c.startsWith("xyz") && d.startsWith("xyz");
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(double[] arr) {
return arr[1] >= 5 && arr[3] >= 5 && arr[7] >= 5 && arr[9] <caret>>= 5;
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(String a, String b, String c, String d) {
return !a.startsWith("xyz") &<caret>& !b.startsWith("xyz") && !c.startsWith("xyz") && !d.startsWith("xyz");
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "true"
class Test {
String foo(String a, String b, String c, String d) {
return a.trim()+b.trim()+c.trim()+<caret>d.trim();
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "true"
class Test {
String foo(int a, int b, int c, int d) {
return a * 2 + "|" + b * 2 + "|" + c * 2 + "|"<caret> + d * 2;
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "true"
class Test {
boolean foo(String a, String b, String c, String d) {
return !(a.startsWith("xyz") |<caret>| b.startsWith("xyz") || c.startsWith("xyz") || d.startsWith("xyz"));
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "false"
class Test {
boolean foo(String a, String b, String c, String d) {
return a == null || b == null || c == null || d == <caret>null;
}
}

View File

@@ -0,0 +1,6 @@
// "Fold expression into Stream chain" "true"
class Test {
int foo(String a, String b, String c, String d) {
return a.length()+b.length()+c.length()+<caret>d.length();
}
}

View File

@@ -0,0 +1,24 @@
// 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.streamMigration.FoldExpressionIntoStreamInspection;
import com.intellij.codeInspection.streamToLoop.StreamToLoopInspection;
import org.jetbrains.annotations.NotNull;
public class FoldExpressionIntoStreamInspectionTest extends LightQuickFixParameterizedTestCase {
@NotNull
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new FoldExpressionIntoStreamInspection()};
}
public void test() { doAllTests(); }
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/foldIntoStream";
}
}

View File

@@ -954,3 +954,5 @@ inspection.capturing.cleaner.description=Cleaner captures object reference
inspection.redundant.explicit.close=Redundant close
inspection.redundant.explicit.close.fix.name=Remove redundant close
inspection.fold.expression.into.stream.display.name=Expression can be folded into Stream chain
inspection.fold.expression.into.stream.fix.name=Fold expression into Stream chain

View File

@@ -703,7 +703,11 @@ public class EquivalenceChecker {
if (qualifier2 == null) {
return EXACT_MISMATCH;
}
return expressionsMatch(qualifier1, qualifier2);
Match match = expressionsMatch(qualifier1, qualifier2);
if (match.isExactMismatch()) {
return new Match(qualifier1, qualifier2);
}
return match;
}
else {
if (qualifier2 != null && !(qualifier2 instanceof PsiThisExpression || qualifier2 instanceof PsiSuperExpression)) {

View File

@@ -74,6 +74,26 @@ public class StreamApiUtil {
return true;
}
/**
* Returns a Stream API class name (Stream, LongStream, IntStream, DoubleStream) which corresponds to given element type,
* or null if there's no corresponding Stream API class.
*
* @param type stream element type
* @return a fully-qualified class name
*/
@Contract("null -> null")
@Nullable
public static String getStreamClassForType(@Nullable PsiType type) {
if(type == null) return null;
if(type instanceof PsiPrimitiveType) {
if(type.equals(PsiType.INT)) return CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM;
if(type.equals(PsiType.LONG)) return CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM;
if(type.equals(PsiType.DOUBLE)) return CommonClassNames.JAVA_UTIL_STREAM_DOUBLE_STREAM;
return null;
}
return CommonClassNames.JAVA_UTIL_STREAM_STREAM;
}
/**
* Returns call from call chain which name satisfies isWantedCall predicate.
* Also checks that all calls between start call and wanted call satisfies isAllowedIntermediateCall