/* * 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 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() { @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 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; } }