blob: d4beda08c87593ad06a4604eb723e905c0af9c82 [file] [log] [blame]
/*
* Copyright 2000-2014 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.ExceptionUtil;
import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.controlFlow.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.IntArrayList;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
/**
* User: anna
*/
public class StreamApiMigrationInspection extends BaseJavaBatchLocalInspectionTool {
private static final Logger LOG = Logger.getInstance("#" + StreamApiMigrationInspection.class.getName());
@Nls
@NotNull
@Override
public String getGroupDisplayName() {
return GroupNames.LANGUAGE_LEVEL_SPECIFIC_GROUP_NAME;
}
@Nls
@NotNull
@Override
public String getDisplayName() {
return "foreach loop can be collapsed with stream api";
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@NotNull
@Override
public String getShortName() {
return "Convert2streamapi";
}
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitForeachStatement(PsiForeachStatement statement) {
super.visitForeachStatement(statement);
if (PsiUtil.getLanguageLevel(statement).isAtLeast(LanguageLevel.JDK_1_8)) {
final PsiExpression iteratedValue = statement.getIteratedValue();
final PsiStatement body = statement.getBody();
if (iteratedValue != null && body != null) {
final PsiType iteratedValueType = iteratedValue.getType();
if (InheritanceUtil.isInheritor(iteratedValueType, CommonClassNames.JAVA_UTIL_COLLECTION)) {
final PsiClass iteratorClass = PsiUtil.resolveClassInType(iteratedValueType);
LOG.assertTrue(iteratorClass != null);
try {
final ControlFlow controlFlow = ControlFlowFactory.getInstance(holder.getProject())
.getControlFlow(body, LocalsOrMyInstanceFieldsControlFlowPolicy.getInstance());
int startOffset = controlFlow.getStartOffset(body);
int endOffset = controlFlow.getEndOffset(body);
final Collection<PsiStatement> exitPoints = ControlFlowUtil
.findExitPointsAndStatements(controlFlow, startOffset, endOffset, new IntArrayList(), PsiContinueStatement.class,
PsiBreakStatement.class, PsiReturnStatement.class, PsiThrowStatement.class);
if (exitPoints.isEmpty()) {
final boolean[] effectivelyFinal = new boolean[] {true};
body.accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
if (!effectivelyFinal[0]) return;
super.visitElement(element);
}
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
if (!effectivelyFinal[0]) return;
super.visitReferenceExpression(expression);
final PsiElement resolve = expression.resolve();
if (resolve instanceof PsiVariable && !(resolve instanceof PsiField)) {
effectivelyFinal[0] = HighlightControlFlowUtil.isEffectivelyFinal((PsiVariable)resolve, body, expression);
}
}
});
if (effectivelyFinal[0]) {
if (isCollectCall(body)) {
holder.registerProblem(iteratedValue, "Can be replaced with collect call",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, new ReplaceWithCollectCallFix());
} else if (!isTrivial(body, statement.getIterationParameter(), iteratedValueType)) {
holder.registerProblem(iteratedValue, "Can be replaced with foreach call",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, new ReplaceWithForeachCallFix());
}
}
}
}
catch (AnalysisCanceledException ignored) {
}
}
}
}
}
};
}
private static boolean isCollectCall(PsiStatement body) {
final PsiIfStatement ifStatement = extractIfStatement(body);
final PsiMethodCallExpression methodCallExpression = extractAddCall(body);
if (methodCallExpression != null) {
final PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression();
final PsiExpression qualifierExpression = methodExpression.getQualifierExpression();
PsiClass qualifierClass = null;
if (qualifierExpression instanceof PsiReferenceExpression) {
qualifierClass = PsiUtil.resolveClassInType(qualifierExpression.getType());
} else if (qualifierExpression == null) {
final PsiClass enclosingClass = PsiTreeUtil.getParentOfType(body, PsiClass.class);
if (PsiUtil.getEnclosingStaticElement(body, enclosingClass) == null) {
qualifierClass = enclosingClass;
}
}
if (qualifierClass != null &&
InheritanceUtil.isInheritor(qualifierClass, false, CommonClassNames.JAVA_UTIL_COLLECTION)) {
if (ifStatement != null) {
final PsiExpression condition = ifStatement.getCondition();
if (condition != null && isConditionDependsOnUpdatedCollections(condition, qualifierExpression)) return false;
}
final PsiElement resolve = methodExpression.resolve();
if (resolve instanceof PsiMethod &&
"add".equals(((PsiMethod)resolve).getName()) &&
((PsiMethod)resolve).getParameterList().getParametersCount() == 1) {
final PsiExpression[] args = methodCallExpression.getArgumentList().getExpressions();
if (args.length == 1) {
if (args[0] instanceof PsiCallExpression) {
final PsiMethod method = ((PsiCallExpression)args[0]).resolveMethod();
return method != null && !method.hasTypeParameters() && !isThrowsCompatible(method);
}
return true;
}
}
}
}
return false;
}
private static boolean isConditionDependsOnUpdatedCollections(PsiExpression condition,
PsiExpression qualifierExpression) {
final PsiElement collection = qualifierExpression != null
? ((PsiReferenceExpression)qualifierExpression).resolve()
: null;
final boolean[] dependsOnCollection = {false};
condition.accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
super.visitReferenceExpression(expression);
if (collection != null && collection == expression.resolve()) {
dependsOnCollection[0] = true;
}
}
@Override
public void visitMethodCallExpression(PsiMethodCallExpression expression) {
super.visitMethodCallExpression(expression);
final PsiExpression callQualifier = expression.getMethodExpression().getQualifierExpression();
if (collection == callQualifier) {
dependsOnCollection[0] = true;
}
if (collection == null && (callQualifier instanceof PsiThisExpression && ((PsiThisExpression)callQualifier).getQualifier() == null ||
callQualifier instanceof PsiSuperExpression && ((PsiSuperExpression)callQualifier).getQualifier() == null)) {
dependsOnCollection[0] = true;
}
}
@Override
public void visitThisExpression(PsiThisExpression expression) {
super.visitThisExpression(expression);
if (collection == null && expression.getQualifier() == null && expression.getParent() instanceof PsiExpressionList) {
dependsOnCollection[0] = true;
}
}
@Override
public void visitClass(PsiClass aClass) {}
});
return dependsOnCollection[0];
}
private static boolean isTrivial(PsiStatement body, PsiParameter parameter, PsiType iteratedValueType) {
final PsiIfStatement ifStatement = extractIfStatement(body);
//stream
if (ifStatement != null &&
InheritanceUtil.isInheritor(iteratedValueType, CommonClassNames.JAVA_UTIL_COLLECTION)) {
return false;
}
//method reference
final PsiCallExpression callExpression = LambdaCanBeMethodReferenceInspection
.canBeMethodReferenceProblem(body instanceof PsiBlockStatement ? ((PsiBlockStatement)body).getCodeBlock() : body,
new PsiParameter[]{parameter}, null);
if (callExpression == null) {
return true;
}
final PsiMethod method = callExpression.resolveMethod();
return method != null && isThrowsCompatible(method);
}
private static boolean isThrowsCompatible(PsiMethod method) {
return ContainerUtil.find(method.getThrowsList().getReferencedTypes(), new Condition<PsiClassType>() {
@Override
public boolean value(PsiClassType type) {
return !ExceptionUtil.isUncheckedException(type);
}
}) != null;
}
private static class ReplaceWithForeachCallFix implements LocalQuickFix {
@NotNull
@Override
public String getName() {
return getFamilyName();
}
@NotNull
@Override
public String getFamilyName() {
return "Replace with forEach";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiForeachStatement foreachStatement = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PsiForeachStatement.class);
if (foreachStatement != null) {
PsiStatement body = foreachStatement.getBody();
final PsiExpression iteratedValue = foreachStatement.getIteratedValue();
if (body != null && iteratedValue != null) {
final Collection<PsiComment> comments = PsiTreeUtil.findChildrenOfType(body, PsiComment.class);
final PsiElement parent = foreachStatement.getParent();
for (PsiElement comment : PsiTreeUtil.findChildrenOfType(body, PsiComment.class)) {
parent.addBefore(comment, foreachStatement);
}
final PsiParameter parameter = foreachStatement.getIterationParameter();
final PsiIfStatement ifStmt = extractIfStatement(body);
String foreEachText = wrapInBlock(body);
String iterated = getIteratedValueText(iteratedValue);
if (ifStmt != null) {
final PsiExpression condition = ifStmt.getCondition();
if (condition != null) {
final PsiStatement thenBranch = ifStmt.getThenBranch();
LOG.assertTrue(thenBranch != null);
if (InheritanceUtil.isInheritor(iteratedValue.getType(), CommonClassNames.JAVA_UTIL_COLLECTION)) {
body = thenBranch;
foreEachText = wrapInBlock(thenBranch);
iterated += ".stream().filter(" + parameter.getName() + " -> " + condition.getText() +")";
}
}
}
final PsiParameter[] parameters = {parameter};
String methodReferenceText = null;
final PsiCallExpression callExpression = LambdaCanBeMethodReferenceInspection.extractMethodCallFromBlock(body);
if (callExpression != null) {
final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
final PsiClass consumerClass = psiFacade.findClass("java.util.function.Consumer", GlobalSearchScope.allScope(project));
final PsiClassType functionalType = consumerClass != null ? psiFacade.getElementFactory().createType(consumerClass, callExpression.getType()) : null;
final PsiCallExpression toConvertCall = LambdaCanBeMethodReferenceInspection.canBeMethodReferenceProblem(body instanceof PsiBlockStatement ? ((PsiBlockStatement)body).getCodeBlock() : body, parameters, functionalType);
methodReferenceText = LambdaCanBeMethodReferenceInspection.createMethodReferenceText(toConvertCall, functionalType, parameters);
}
final String lambdaText = parameter.getName() + " -> " + foreEachText;
final String codeBlock8 = methodReferenceText != null ? methodReferenceText : lambdaText;
PsiExpressionStatement callStatement = (PsiExpressionStatement)JavaPsiFacade.getElementFactory(project).createStatementFromText(iterated + ".forEach(" + codeBlock8 + ");", foreachStatement);
callStatement = (PsiExpressionStatement)foreachStatement.replace(callStatement);
final PsiExpressionList argumentList = ((PsiCallExpression)callStatement.getExpression()).getArgumentList();
LOG.assertTrue(argumentList != null, callStatement.getText());
final PsiExpression[] expressions = argumentList.getExpressions();
LOG.assertTrue(expressions.length == 1);
if (expressions[0] instanceof PsiLambdaExpression && ((PsiLambdaExpression)expressions[0]).getFunctionalInterfaceType() == null ||
expressions[0] instanceof PsiMethodReferenceExpression && ((PsiMethodReferenceExpression)expressions[0]).getFunctionalInterfaceType() == null) {
callStatement = (PsiExpressionStatement)callStatement.replace(JavaPsiFacade.getElementFactory(project).createStatementFromText(iterated + ".forEach((" + parameter.getText() + ") -> " + foreEachText + ");", callStatement));
}
}
}
}
private static String wrapInBlock(PsiStatement body) {
if (body instanceof PsiExpressionStatement) {
return ((PsiExpressionStatement)body).getExpression().getText();
}
final String bodyText = body.getText();
if (!(body instanceof PsiBlockStatement)) {
return "{" + bodyText + "}";
}
return bodyText;
}
}
private static class ReplaceWithCollectCallFix implements LocalQuickFix {
@NotNull
@Override
public String getName() {
return getFamilyName();
}
@NotNull
@Override
public String getFamilyName() {
return "Replace with collect";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiForeachStatement foreachStatement = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PsiForeachStatement.class);
if (foreachStatement != null) {
PsiStatement body = foreachStatement.getBody();
final PsiExpression iteratedValue = foreachStatement.getIteratedValue();
if (body != null && iteratedValue != null) {
final PsiParameter parameter = foreachStatement.getIterationParameter();
final PsiIfStatement ifStatement = extractIfStatement(body);
final PsiMethodCallExpression methodCallExpression = extractAddCall(body);
String iteration = getIteratedValueText(iteratedValue) + ".stream()";
if (ifStatement != null) {
final PsiExpression condition = ifStatement.getCondition();
if (condition != null) {
iteration += ".filter(" + parameter.getName() + " -> " + condition.getText() +")";
}
}
final PsiExpression mapperCall = methodCallExpression.getArgumentList().getExpressions()[0];
if (!isIdentityMapping(parameter, mapperCall)) {
iteration +=".map(";
final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
final PsiClass functionClass = psiFacade.findClass("java.util.function.Function", GlobalSearchScope.allScope(project));
final PsiClassType functionalInterfaceType = functionClass != null ? psiFacade.getElementFactory().createType(functionClass, parameter.getType(), mapperCall.getType()) : null;
final PsiCallExpression toConvertCall = LambdaCanBeMethodReferenceInspection.canBeMethodReferenceProblem(mapperCall, new PsiParameter[]{parameter}, functionalInterfaceType);
final String methodReferenceText = LambdaCanBeMethodReferenceInspection.createMethodReferenceText(toConvertCall, functionalInterfaceType, new PsiParameter[]{parameter});
if (methodReferenceText != null) {
iteration += methodReferenceText;
} else {
iteration += parameter.getName() + " -> " + mapperCall.getText();
}
iteration += ")";
}
iteration += ".collect(java.util.stream.Collectors.";
String variableName = null;
PsiExpression primitiveInitializer = null;
final PsiExpression qualifierExpression = methodCallExpression.getMethodExpression().getQualifierExpression();
if (qualifierExpression instanceof PsiReferenceExpression) {
final PsiElement resolve = ((PsiReferenceExpression)qualifierExpression).resolve();
if (resolve instanceof PsiVariable) {
if (resolve instanceof PsiLocalVariable && foreachStatement.equals(PsiTreeUtil.skipSiblingsForward(resolve.getParent(), PsiWhiteSpace.class))) {
final PsiExpression initializer = ((PsiVariable)resolve).getInitializer();
if (initializer instanceof PsiNewExpression) {
final PsiExpressionList argumentList = ((PsiNewExpression)initializer).getArgumentList();
if (argumentList != null && argumentList.getExpressions().length == 0) {
primitiveInitializer = initializer;
}
}
}
variableName = qualifierExpression.getText() + ".";
}
} else if (qualifierExpression == null) {
variableName = "";
}
if (variableName != null) {
final PsiElement parent = foreachStatement.getParent();
for (PsiElement comment : PsiTreeUtil.findChildrenOfType(body, PsiComment.class)) {
parent.addBefore(comment, foreachStatement);
}
}
PsiElement result = null;
if (primitiveInitializer != null) {
final PsiType initializerType = primitiveInitializer.getType();
final PsiClassType rawType = initializerType instanceof PsiClassType ? ((PsiClassType)initializerType).rawType() : null;
if (rawType != null && rawType.equalsToText(CommonClassNames.JAVA_UTIL_ARRAY_LIST)) {
iteration += "toList()";
} else if (rawType != null && rawType.equalsToText(CommonClassNames.JAVA_UTIL_HASH_SET)) {
iteration += "toSet()";
} else {
iteration += "toCollection(() -> " + primitiveInitializer.getText() +")";
}
iteration += ")";
result = primitiveInitializer.replace(JavaPsiFacade.getElementFactory(project).createExpressionFromText(iteration, foreachStatement));
foreachStatement.delete();
} else if (variableName != null){
iteration += "toList())";
result = foreachStatement.replace(JavaPsiFacade.getElementFactory(project).createStatementFromText(variableName + "addAll(" + iteration +");", foreachStatement));
}
if (result != null) {
result = JavaCodeStyleManager.getInstance(project).shortenClassReferences(result);
}
}
}
}
private static boolean isIdentityMapping(PsiParameter parameter, PsiExpression mapperCall) {
return mapperCall instanceof PsiReferenceExpression && ((PsiReferenceExpression)mapperCall).resolve() == parameter;
}
}
private static String getIteratedValueText(PsiExpression iteratedValue) {
return iteratedValue instanceof PsiCallExpression ||
iteratedValue instanceof PsiReferenceExpression ||
iteratedValue instanceof PsiQualifiedExpression ||
iteratedValue instanceof PsiParenthesizedExpression ? iteratedValue.getText() : "(" + iteratedValue.getText() + ")";
}
public static PsiIfStatement extractIfStatement(PsiStatement body) {
PsiIfStatement ifStmt = null;
if (body instanceof PsiIfStatement) {
ifStmt = (PsiIfStatement)body;
} else if (body instanceof PsiBlockStatement) {
final PsiStatement[] statements = ((PsiBlockStatement)body).getCodeBlock().getStatements();
if (statements.length == 1 && statements[0] instanceof PsiIfStatement) {
ifStmt = (PsiIfStatement)statements[0];
}
}
if (ifStmt != null && ifStmt.getElseBranch() == null && ifStmt.getThenBranch() != null) {
return ifStmt;
}
return null;
}
private static PsiMethodCallExpression extractAddCall(PsiStatement body) {
final PsiIfStatement ifStatement = extractIfStatement(body);
if (ifStatement != null) {
return extractAddCall(ifStatement.getThenBranch());
}
PsiExpressionStatement stmt = null;
if (body instanceof PsiBlockStatement) {
final PsiStatement[] statements = ((PsiBlockStatement)body).getCodeBlock().getStatements();
if (statements.length == 1 && statements[0] instanceof PsiExpressionStatement) {
stmt = (PsiExpressionStatement)statements[0];
}
}
else if (body instanceof PsiExpressionStatement) {
stmt = (PsiExpressionStatement)body;
}
if (stmt != null) {
final PsiExpression expression = stmt.getExpression();
if (expression instanceof PsiMethodCallExpression) {
return (PsiMethodCallExpression)expression;
}
}
return null;
}
}