/* * 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.streamMigration; import com.intellij.codeHighlighting.HighlightDisplayLevel; import com.intellij.codeInsight.ExceptionUtil; import com.intellij.codeInsight.PsiEquivalenceUtil; import com.intellij.codeInsight.daemon.GroupNames; import com.intellij.codeInsight.daemon.HighlightDisplayKey; import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil; import com.intellij.codeInspection.*; import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.profile.codeInspection.InspectionProjectProfileManager; import com.intellij.psi.*; import com.intellij.psi.controlFlow.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.LocalSearchScope; import com.intellij.psi.search.searches.ReferencesSearch; 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.intellij.psi.util.TypeConversionUtil; import com.intellij.refactoring.util.RefactoringUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.IntArrayList; import com.siyeh.ig.psiutils.BoolUtils; import com.siyeh.ig.psiutils.ControlFlowUtils; import com.siyeh.ig.psiutils.ExpressionUtils; import com.siyeh.ig.psiutils.ParenthesesUtils; import one.util.streamex.EntryStream; import one.util.streamex.StreamEx; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; import static com.intellij.codeInspection.streamMigration.StreamApiMigrationInspection.InitializerUsageStatus.*; /** * User: anna */ public class StreamApiMigrationInspection extends BaseJavaBatchLocalInspectionTool { private static final Logger LOG = Logger.getInstance("#" + StreamApiMigrationInspection.class.getName()); static final Map COLLECTION_TO_ARRAY = EntryStream.of( CommonClassNames.JAVA_UTIL_ARRAY_LIST, "toArray", "java.util.LinkedList", "toArray", CommonClassNames.JAVA_UTIL_HASH_SET, "distinct().toArray", "java.util.LinkedHashSet", "distinct().toArray", "java.util.TreeSet", "distinct().sorted().toArray" ).toMap(); public boolean REPLACE_TRIVIAL_FOREACH; public boolean SUGGEST_FOREACH; private HighlightDisplayKey myKey; @Nullable @Override public JComponent createOptionsPanel() { MultipleCheckboxOptionsPanel panel = new MultipleCheckboxOptionsPanel(this); panel.addCheckbox("Suggest to replace with forEach or forEachOrdered", "SUGGEST_FOREACH"); panel.addCheckbox("Replace trivial foreach statements", "REPLACE_TRIVIAL_FOREACH"); return panel; } @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) { if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) { return PsiElementVisitor.EMPTY_VISITOR; } return new StreamApiMigrationVisitor(holder, isOnTheFly); } @Contract("_, null -> false") static boolean isVariableReferenced(PsiVariable variable, PsiExpression value) { return value != null && ReferencesSearch.search(variable, new LocalSearchScope(value)).findFirst() != null; } static boolean isReferencedInOperations(PsiElement element, TerminalBlock tb) { return ReferencesSearch.search(element, new LocalSearchScope(tb.intermediateAndSourceExpressions().toArray(PsiElement[]::new))) .findFirst() != null; } @Nullable static PsiReturnStatement getNextReturnStatement(PsiStatement statement) { PsiElement nextStatement = PsiTreeUtil.skipSiblingsForward(statement, PsiWhiteSpace.class, PsiComment.class); if(nextStatement instanceof PsiReturnStatement) return (PsiReturnStatement)nextStatement; PsiElement parent = statement.getParent(); if(parent instanceof PsiCodeBlock) { PsiStatement[] statements = ((PsiCodeBlock)parent).getStatements(); if(statements.length == 0 || statements[statements.length-1] != statement) return null; parent = parent.getParent(); if(!(parent instanceof PsiBlockStatement)) return null; parent = parent.getParent(); } if(parent instanceof PsiIfStatement) return getNextReturnStatement((PsiStatement)parent); return null; } @Contract("null, null -> true; null, !null -> false") private static boolean sameReference(PsiExpression expr1, PsiExpression expr2) { if(expr1 == null && expr2 == null) return true; if (!(expr1 instanceof PsiReferenceExpression) || !(expr2 instanceof PsiReferenceExpression)) return false; PsiReferenceExpression ref1 = (PsiReferenceExpression)expr1; PsiReferenceExpression ref2 = (PsiReferenceExpression)expr2; return Objects.equals(ref1.getReferenceName(), ref2.getReferenceName()) && sameReference(ref1.getQualifierExpression(), ref2.getQualifierExpression()); } @Nullable static PsiExpression extractAddend(PsiAssignmentExpression assignment) { if(JavaTokenType.PLUSEQ.equals(assignment.getOperationTokenType())) { return assignment.getRExpression(); } else if(JavaTokenType.EQ.equals(assignment.getOperationTokenType())) { if (assignment.getRExpression() instanceof PsiBinaryExpression) { PsiBinaryExpression binOp = (PsiBinaryExpression)assignment.getRExpression(); if(JavaTokenType.PLUS.equals(binOp.getOperationTokenType())) { if(sameReference(binOp.getLOperand(), assignment.getLExpression())) { return binOp.getROperand(); } if(sameReference(binOp.getROperand(), assignment.getLExpression())) { return binOp.getLOperand(); } } } } return null; } @Nullable static PsiVariable extractAccumulator(PsiAssignmentExpression assignment) { if(!(assignment.getLExpression() instanceof PsiReferenceExpression)) return null; PsiReferenceExpression lExpr = (PsiReferenceExpression)assignment.getLExpression(); PsiElement accumulator = lExpr.resolve(); if(!(accumulator instanceof PsiVariable)) return null; PsiVariable var = (PsiVariable)accumulator; if(JavaTokenType.PLUSEQ.equals(assignment.getOperationTokenType())) { return var; } else if(JavaTokenType.EQ.equals(assignment.getOperationTokenType())) { if (assignment.getRExpression() instanceof PsiBinaryExpression) { PsiBinaryExpression binOp = (PsiBinaryExpression)assignment.getRExpression(); if(JavaTokenType.PLUS.equals(binOp.getOperationTokenType())) { PsiExpression left = binOp.getLOperand(); PsiExpression right = binOp.getROperand(); if (sameReference(left, lExpr) || sameReference(right, lExpr)) { return var; } } } } return null; } @Contract("null -> null") static PsiExpression extractIncrementedLValue(PsiExpression expression) { if(expression instanceof PsiPostfixExpression) { if(JavaTokenType.PLUSPLUS.equals(((PsiPostfixExpression)expression).getOperationTokenType())) { return ((PsiPostfixExpression)expression).getOperand(); } } else if(expression instanceof PsiPrefixExpression) { if(JavaTokenType.PLUSPLUS.equals(((PsiPrefixExpression)expression).getOperationTokenType())) { return ((PsiPrefixExpression)expression).getOperand(); } } else if(expression instanceof PsiAssignmentExpression) { PsiAssignmentExpression assignment = (PsiAssignmentExpression)expression; if(ExpressionUtils.isLiteral(extractAddend(assignment), 1)) { return assignment.getLExpression(); } } return null; } @Nullable private static PsiLocalVariable getIncrementedVariable(TerminalBlock tb, List variables) { // have only one non-final variable if(variables.size() != 1) return null; // have single expression which is either ++x or x++ or x+=1 or x=x+1 PsiExpression operand = extractIncrementedLValue(tb.getSingleExpression(PsiExpression.class)); if(!(operand instanceof PsiReferenceExpression)) return null; PsiElement element = ((PsiReferenceExpression)operand).resolve(); // the referred variable is the same as non-final variable and not used in intermediate operations if(!(element instanceof PsiLocalVariable) || !variables.contains(element) || isReferencedInOperations(element, tb)) return null; return (PsiLocalVariable)element; } @Nullable private static PsiVariable getAccumulatedVariable(TerminalBlock tb, List variables) { // have only one non-final variable if(variables.size() != 1) return null; PsiAssignmentExpression assignment = tb.getSingleExpression(PsiAssignmentExpression.class); if(assignment == null) return null; PsiVariable var = extractAccumulator(assignment); // the referred variable is the same as non-final variable if(var == null || !variables.contains(var)) return null; if (!(var.getType() instanceof PsiPrimitiveType) || var.getType().equalsToText("float")) return null; // the referred variable is not used in intermediate operations if(isReferencedInOperations(var, tb)) return null; PsiExpression addend = extractAddend(assignment); LOG.assertTrue(addend != null); if(ReferencesSearch.search(var, new LocalSearchScope(addend)).findFirst() != null) return null; return var; } static boolean isAddAllCall(TerminalBlock tb) { final PsiVariable variable = tb.getVariable(); final PsiMethodCallExpression methodCallExpression = tb.getSingleMethodCall(); LOG.assertTrue(methodCallExpression != null); if (!isIdentityMapping(variable, methodCallExpression.getArgumentList().getExpressions()[0])) return false; PsiExpression qualifierExpression = methodCallExpression.getMethodExpression().getQualifierExpression(); if(qualifierExpression == null || qualifierExpression instanceof PsiThisExpression) { PsiMethod method = PsiTreeUtil.getParentOfType(methodCallExpression, PsiMethod.class); return method == null || !method.getName().equals("addAll"); } return true; } private static boolean isCollectCall(TerminalBlock tb) { final PsiMethodCallExpression methodCallExpression = tb.getSingleMethodCall(); if (methodCallExpression != null) { final PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression(); final PsiExpression qualifierExpression = methodExpression.getQualifierExpression(); PsiClass qualifierClass = null; if (qualifierExpression instanceof PsiReferenceExpression) { if (ReferencesSearch.search(tb.getVariable(), new LocalSearchScope(qualifierExpression)).findFirst() != null) { return false; } final PsiElement resolve = ((PsiReferenceExpression)qualifierExpression).resolve(); if (resolve instanceof PsiVariable) { if (ReferencesSearch.search(resolve, new LocalSearchScope(methodCallExpression.getArgumentList())).findFirst() != null) { return false; } } qualifierClass = PsiUtil.resolveClassInType(qualifierExpression.getType()); } else if (qualifierExpression == null || qualifierExpression instanceof PsiThisExpression) { final PsiClass enclosingClass = PsiTreeUtil.getParentOfType(methodCallExpression, PsiClass.class); if (PsiUtil.getEnclosingStaticElement(methodCallExpression, enclosingClass) == null) { qualifierClass = enclosingClass; } } if (qualifierClass != null && InheritanceUtil.isInheritor(qualifierClass, false, CommonClassNames.JAVA_UTIL_COLLECTION)) { if (tb.intermediateExpressions().anyMatch(expression -> isExpressionDependsOnUpdatedCollections(expression, 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 isExpressionDependsOnUpdatedCollections(PsiExpression condition, PsiExpression qualifierExpression) { final PsiElement collection = qualifierExpression instanceof PsiReferenceExpression ? ((PsiReferenceExpression)qualifierExpression).resolve() : null; if (collection != null) { return ReferencesSearch.search(collection, new LocalSearchScope(condition)).findFirst() != null; } final boolean[] dependsOnCollection = {false}; condition.accept(new JavaRecursiveElementWalkingVisitor() { @Override public void visitMethodCallExpression(PsiMethodCallExpression expression) { super.visitMethodCallExpression(expression); final PsiExpression callQualifier = expression.getMethodExpression().getQualifierExpression(); if (callQualifier == 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 (expression.getQualifier() == null && expression.getParent() instanceof PsiExpressionList) { dependsOnCollection[0] = true; } } @Override public void visitClass(PsiClass aClass) {} @Override public void visitLambdaExpression(PsiLambdaExpression expression) {} }); return dependsOnCollection[0]; } @Contract("_, null -> false") private static boolean isTrivial(PsiStatement body, PsiLoopStatement loopStatement) { if(!(loopStatement instanceof PsiForeachStatement)) return false; PsiParameter parameter = ((PsiForeachStatement)loopStatement).getIterationParameter(); //method reference final PsiExpression candidate = new LambdaCanBeMethodReferenceInspection() .canBeMethodReferenceProblem(body instanceof PsiBlockStatement ? ((PsiBlockStatement)body).getCodeBlock() : body, new PsiParameter[]{parameter}, createDefaultConsumerType(parameter.getProject(), parameter), null); if (!(candidate instanceof PsiCallExpression)) { return true; } final PsiMethod method = ((PsiCallExpression)candidate).resolveMethod(); return method != null && isThrowsCompatible(method); } static boolean isSupported(PsiType type) { if(type instanceof PsiPrimitiveType) { return type.equals(PsiType.INT) || type.equals(PsiType.LONG) || type.equals(PsiType.DOUBLE); } return true; } private static boolean isThrowsCompatible(PsiMethod method) { return ContainerUtil.find(method.getThrowsList().getReferencedTypes(), type -> !ExceptionUtil.isUncheckedException(type)) != null; } @Contract("_, null -> false") static boolean isIdentityMapping(PsiVariable variable, PsiExpression mapperCall) { return mapperCall instanceof PsiReferenceExpression && ((PsiReferenceExpression)mapperCall).isReferenceTo(variable); } @Nullable private static PsiClassType createDefaultConsumerType(Project project, PsiVariable variable) { final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project); final PsiClass consumerClass = psiFacade.findClass("java.util.function.Consumer", GlobalSearchScope.allScope(project)); return consumerClass != null ? psiFacade.getElementFactory().createType(consumerClass, variable.getType()) : null; } static boolean isVariableSuitableForStream(PsiVariable variable, PsiStatement statement, TerminalBlock tb) { if(ReferencesSearch.search(variable).forEach(ref -> { PsiElement element = ref.getElement(); return !(element instanceof PsiExpression) || !PsiUtil.isAccessedForWriting((PsiExpression)element) || tb.operations().anyMatch(op -> op.isWriteAllowed(variable, (PsiExpression)element)); })) { return true; } return HighlightControlFlowUtil.isEffectivelyFinal(variable, statement, null); } @Contract("null -> null") static PsiLocalVariable extractCollectionVariable(PsiExpression qualifierExpression) { if (qualifierExpression instanceof PsiReferenceExpression) { final PsiElement resolve = ((PsiReferenceExpression)qualifierExpression).resolve(); if (resolve instanceof PsiLocalVariable) { PsiLocalVariable var = (PsiLocalVariable)resolve; final PsiExpression initializer = var.getInitializer(); if (initializer instanceof PsiNewExpression) { final PsiExpressionList argumentList = ((PsiNewExpression)initializer).getArgumentList(); if (argumentList != null && argumentList.getExpressions().length == 0) { return var; } } } } return null; } /** * Checks whether variable can be referenced between start and loop entry. Back-edges are also considered, so the actual place * where it referenced might be outside of (start, loop entry) interval. * * @param flow ControlFlow to analyze * @param start start point * @param loop loop to check * @param variable variable to analyze * @return true if variable can be referenced between start and stop points */ private static boolean isVariableReferencedBeforeLoopEntry(final ControlFlow flow, final int start, final PsiLoopStatement loop, final PsiVariable variable) { final int loopStart = flow.getStartOffset(loop); final int loopEnd = flow.getEndOffset(loop); if(start == loopStart) return false; List edges = ControlFlowUtil.getEdges(flow, start); // DFS visits instructions mainly in backward direction while here visiting in forward direction // greatly reduces number of iterations. Collections.reverse(edges); BitSet referenced = new BitSet(); boolean changed = true; while(changed) { changed = false; for(ControlFlowUtil.ControlFlowEdge edge: edges) { int from = edge.myFrom; int to = edge.myTo; if(referenced.get(from)) { // jump to the loop start from within the loop is not considered as loop entry if(to == loopStart && (from < loopStart || from >= loopEnd)) { return true; } if(!referenced.get(to)) { referenced.set(to); changed = true; } continue; } if(ControlFlowUtil.isVariableAccess(flow, from, variable)) { referenced.set(from); referenced.set(to); if(to == loopStart) return true; changed = true; } } } return false; } enum InitializerUsageStatus { // Variable is declared just before the wanted place DECLARED_JUST_BEFORE, // All initial value usages go through wanted place and at wanted place the variable value is guaranteed to be the initial value AT_WANTED_PLACE_ONLY, // At wanted place the variable value is guaranteed to be the initial value, but this initial value might be used somewhere else AT_WANTED_PLACE, // It's not guaranteed that the variable value at wanted place is initial value UNKNOWN } static InitializerUsageStatus getInitializerUsageStatus(PsiVariable var, PsiLoopStatement nextStatement) { if(!(var instanceof PsiLocalVariable) || var.getInitializer() == null) return UNKNOWN; if(isDeclarationJustBefore(var, nextStatement)) return DECLARED_JUST_BEFORE; // Check that variable is declared in the same method or the same lambda expression if(PsiTreeUtil.getParentOfType(var, PsiLambdaExpression.class, PsiMethod.class) != PsiTreeUtil.getParentOfType(nextStatement, PsiLambdaExpression.class, PsiMethod.class)) return UNKNOWN; PsiElement block = PsiUtil.getVariableCodeBlock(var, null); if(block == null) return UNKNOWN; final ControlFlow controlFlow; try { controlFlow = ControlFlowFactory.getInstance(nextStatement.getProject()) .getControlFlow(block, LocalsOrMyInstanceFieldsControlFlowPolicy.getInstance()); } catch (AnalysisCanceledException ignored) { return UNKNOWN; } int start = controlFlow.getEndOffset(var.getInitializer())+1; int stop = controlFlow.getStartOffset(nextStatement); if(isVariableReferencedBeforeLoopEntry(controlFlow, start, nextStatement, var)) return UNKNOWN; if (!ControlFlowUtil.isValueUsedWithoutVisitingStop(controlFlow, start, stop, var)) return AT_WANTED_PLACE_ONLY; return var.hasModifierProperty(PsiModifier.FINAL) ? UNKNOWN : AT_WANTED_PLACE; } static boolean isDeclarationJustBefore(PsiVariable var, PsiStatement nextStatement) { PsiElement declaration = var.getParent(); PsiElement nextStatementParent = nextStatement.getParent(); if(nextStatementParent instanceof PsiLabeledStatement) { nextStatement = (PsiStatement)nextStatementParent; } if(declaration instanceof PsiDeclarationStatement) { PsiElement[] elements = ((PsiDeclarationStatement)declaration).getDeclaredElements(); if (ArrayUtil.getLastElement(elements) == var && nextStatement.equals( PsiTreeUtil.skipSiblingsForward(declaration, PsiWhiteSpace.class, PsiComment.class))) { return true; } } return false; } static String tryUnbox(PsiVariable variable) { PsiType type = variable.getType(); String mapOp = null; if(type.equals(PsiType.INT)) { mapOp = "mapToInt"; } else if(type.equals(PsiType.LONG)) { mapOp = "mapToLong"; } else if(type.equals(PsiType.DOUBLE)) { mapOp = "mapToDouble"; } return mapOp == null ? "" : "."+mapOp+"("+variable.getName()+" -> "+variable.getName()+")"; } private class StreamApiMigrationVisitor extends JavaElementVisitor { private final ProblemsHolder myHolder; private final boolean myIsOnTheFly; public StreamApiMigrationVisitor(ProblemsHolder holder, boolean isOnTheFly) { myHolder = holder; myIsOnTheFly = isOnTheFly; } @Override public void visitForeachStatement(PsiForeachStatement statement) { super.visitForeachStatement(statement); processLoop(statement); } @Override public void visitWhileStatement(PsiWhileStatement statement) { super.visitWhileStatement(statement); processLoop(statement); } @Override public void visitForStatement(PsiForStatement statement) { super.visitForStatement(statement); processLoop(statement); } void processLoop(PsiLoopStatement statement) { final PsiStatement body = statement.getBody(); if(body == null) return; StreamSource source = StreamSource.tryCreate(statement); if(source == null) return; if (!ExceptionUtil.getThrownCheckedExceptions(body).isEmpty()) return; TerminalBlock tb = TerminalBlock.from(source, body); if(tb.isEmpty()) return; final ControlFlow controlFlow; try { controlFlow = ControlFlowFactory.getInstance(myHolder.getProject()) .getControlFlow(body, LocalsOrMyInstanceFieldsControlFlowPolicy.getInstance()); } catch (AnalysisCanceledException ignored) { return; } final Collection exitPoints = ControlFlowUtil .findExitPointsAndStatements(controlFlow, tb.getStartOffset(controlFlow), tb.getEndOffset(controlFlow), new IntArrayList(), PsiContinueStatement.class, PsiBreakStatement.class, PsiReturnStatement.class, PsiThrowStatement.class); int startOffset = controlFlow.getStartOffset(body); int endOffset = controlFlow.getEndOffset(body); PsiElement surrounder = PsiTreeUtil.getParentOfType(statement, PsiLambdaExpression.class, PsiClass.class); final List nonFinalVariables = StreamEx.of(ControlFlowUtil.getUsedVariables(controlFlow, startOffset, endOffset)) .remove(variable -> PsiTreeUtil.getParentOfType(variable, PsiLambdaExpression.class, PsiClass.class) != surrounder) .remove(variable -> isVariableSuitableForStream(variable, statement, tb)).toList(); if (exitPoints.isEmpty()) { if(getIncrementedVariable(tb, nonFinalVariables) != null) { registerProblem(statement, "count", new ReplaceWithCountFix()); } if(getAccumulatedVariable(tb, nonFinalVariables) != null) { registerProblem(statement, "sum", new ReplaceWithSumFix()); } if(!nonFinalVariables.isEmpty()) { return; } if (isCollectCall(tb)) { boolean addAll = statement instanceof PsiForeachStatement && !tb.hasOperations() && isAddAllCall(tb); String methodName; if(addAll) { methodName = "addAll"; } else { PsiMethodCallExpression methodCallExpression = tb.getSingleMethodCall(); if(canCollect(statement, methodCallExpression)) { if(extractToArrayExpression(statement, methodCallExpression) != null) methodName = "toArray"; else methodName = "collect"; } else { if (!SUGGEST_FOREACH) return; methodName = "forEach"; } } registerProblem(statement, methodName, new ReplaceWithCollectFix(methodName)); } // do not replace for(T e : arr) {} with Arrays.stream(arr).forEach(e -> {}) even if flag is set else if (SUGGEST_FOREACH && (tb.hasOperations() || (!(source instanceof ArrayStream) && (REPLACE_TRIVIAL_FOREACH || !isTrivial(body, statement))))) { ReplaceWithForeachCallFix forEachFix = new ReplaceWithForeachCallFix("forEach"); LocalQuickFix[] fixes = {forEachFix}; if (tb.hasOperations()) { //for .stream() fixes = new LocalQuickFix[] {forEachFix, new ReplaceWithForeachCallFix("forEachOrdered")}; } registerProblem(statement, "forEach", fixes); } } else { if (!tb.hasOperations() && !REPLACE_TRIVIAL_FOREACH) return; if (nonFinalVariables.isEmpty() && tb.getSingleStatement() instanceof PsiReturnStatement) { handleSingleReturn(statement, tb); } // Source and intermediate ops should not refer to non-final variables if (tb.intermediateAndSourceExpressions() .flatCollection(expr -> PsiTreeUtil.collectElementsOfType(expr, PsiReferenceExpression.class)) .map(PsiReferenceExpression::resolve).anyMatch(nonFinalVariables::contains)) { return; } PsiStatement[] statements = tb.getStatements(); if (statements.length == 2) { PsiStatement breakStatement = statements[1]; if (!ControlFlowUtils.statementBreaksLoop(breakStatement, statement)) { return; } if (ReferencesSearch.search(tb.getVariable(), new LocalSearchScope(statements)).findFirst() == null && exitPoints.size() == 1 && exitPoints.contains(breakStatement)) { registerProblem(statement, "anyMatch", new ReplaceWithMatchFix("anyMatch")); return; } if (nonFinalVariables.isEmpty() && statements[0] instanceof PsiExpressionStatement) { registerProblem(statement, "findFirst", new ReplaceWithFindFirstFix()); } else if (nonFinalVariables.size() == 1) { PsiAssignmentExpression assignment = ExpressionUtils.getAssignment(statements[0]); if(assignment == null) return; PsiExpression lValue = assignment.getLExpression(); if (!(lValue instanceof PsiReferenceExpression)) return; PsiElement var = ((PsiReferenceExpression)lValue).resolve(); if(!(var instanceof PsiVariable) || !nonFinalVariables.contains(var)) return; PsiExpression rValue = assignment.getRExpression(); if(rValue == null || isVariableReferenced((PsiVariable)var, rValue)) return; if(tb.getVariable().getType() instanceof PsiPrimitiveType && !isIdentityMapping(tb.getVariable(), rValue)) return; registerProblem(statement, "findFirst", new ReplaceWithFindFirstFix()); } } } } boolean canCollect(PsiLoopStatement statement, PsiMethodCallExpression methodCallExpression) { if(methodCallExpression == null) return false; PsiLocalVariable variable = extractCollectionVariable(methodCallExpression.getMethodExpression().getQualifierExpression()); if(variable == null) return false; return getInitializerUsageStatus(variable, statement) != UNKNOWN; } void handleSingleReturn(PsiLoopStatement statement, TerminalBlock tb) { PsiReturnStatement returnStatement = (PsiReturnStatement)tb.getSingleStatement(); PsiExpression value = returnStatement.getReturnValue(); PsiReturnStatement nextReturnStatement = getNextReturnStatement(statement); if (nextReturnStatement != null && (ExpressionUtils.isLiteral(value, Boolean.TRUE) || ExpressionUtils.isLiteral(value, Boolean.FALSE))) { boolean foundResult = (boolean)((PsiLiteralExpression)value).getValue(); String methodName; if (foundResult) { methodName = "anyMatch"; } else { methodName = "noneMatch"; Operation lastOp = tb.getLastOperation(); if(lastOp instanceof FilterOp && (((FilterOp)lastOp).isNegated() ^ BoolUtils.isNegation(lastOp.getExpression()))) { methodName = "allMatch"; } } if(nextReturnStatement.getParent() == statement.getParent() || ExpressionUtils.isLiteral(nextReturnStatement.getReturnValue(), !foundResult)) { registerProblem(statement, methodName, new ReplaceWithMatchFix(methodName)); return; } } if (!isVariableReferenced(tb.getVariable(), value)) { Operation lastOp = tb.getLastOperation(); if (!REPLACE_TRIVIAL_FOREACH && lastOp instanceof StreamSource || (lastOp instanceof FilterOp && lastOp.getPreviousOp() instanceof StreamSource)) { return; } registerProblem(statement, "anyMatch", new ReplaceWithMatchFix("anyMatch")); } if(nextReturnStatement != null && ExpressionUtils.isSimpleExpression(nextReturnStatement.getReturnValue()) && (!(tb.getVariable().getType() instanceof PsiPrimitiveType) || isIdentityMapping(tb.getVariable(), value))) { registerProblem(statement, "findFirst", new ReplaceWithFindFirstFix()); } } @NotNull private TextRange getRange(PsiLoopStatement statement) { boolean wholeStatement = false; if(myIsOnTheFly) { if (myKey == null) { myKey = HighlightDisplayKey.find(getShortName()); } if (myKey != null) { InspectionProfile profile = InspectionProjectProfileManager.getInstance(statement.getProject()).getCurrentProfile(); HighlightDisplayLevel level = profile.getErrorLevel(myKey, statement); wholeStatement = HighlightDisplayLevel.DO_NOT_SHOW.equals(level); } } if(statement instanceof PsiForeachStatement) { PsiJavaToken rParenth = ((PsiForeachStatement)statement).getRParenth(); if (wholeStatement && rParenth != null) { return new TextRange(statement.getTextOffset(), rParenth.getTextOffset() + 1); } PsiExpression iteratedValue = ((PsiForeachStatement)statement).getIteratedValue(); LOG.assertTrue(iteratedValue != null); return iteratedValue.getTextRange(); } else if(statement instanceof PsiForStatement) { PsiJavaToken rParenth = ((PsiForStatement)statement).getRParenth(); if (wholeStatement && rParenth != null) { return new TextRange(statement.getTextOffset(), rParenth.getTextOffset() + 1); } PsiStatement initialization = ((PsiForStatement)statement).getInitialization(); LOG.assertTrue(initialization != null); return initialization.getTextRange(); } else if(statement instanceof PsiWhileStatement) { PsiJavaToken rParenth = ((PsiWhileStatement)statement).getRParenth(); if (wholeStatement && rParenth != null) { return new TextRange(statement.getTextOffset(), rParenth.getTextOffset() + 1); } return statement.getFirstChild().getTextRange(); } else { throw new IllegalStateException("Unexpected statement type: "+statement); } } private void registerProblem(PsiLoopStatement statement, String methodName, LocalQuickFix... fixes) { myHolder.registerProblem(statement, getRange(statement).shiftRight(-statement.getTextOffset()), "Can be replaced with '" + methodName + "' call", fixes); } } /** * * @param element sort statement candidate (must be PsiExpressionStatement) * @param list list which should be sorted * @return comparator string representation, empty string if natural order is used or null if given statement is not sort statement */ @Contract(value = "null, _ -> null") static String tryExtractSortComparatorText(PsiElement element, PsiVariable list) { if(!(element instanceof PsiExpressionStatement)) return null; PsiExpression expression = ((PsiExpressionStatement)element).getExpression(); if(!(expression instanceof PsiMethodCallExpression)) return null; PsiMethodCallExpression methodCall = (PsiMethodCallExpression)expression; PsiReferenceExpression methodExpression = methodCall.getMethodExpression(); if(!"sort".equals(methodExpression.getReferenceName())) return null; PsiMethod method = methodCall.resolveMethod(); if(method == null) return null; PsiClass containingClass = method.getContainingClass(); if(containingClass == null) return null; PsiExpression listExpression = null; PsiExpression comparatorExpression = null; if(CommonClassNames.JAVA_UTIL_COLLECTIONS.equals(containingClass.getQualifiedName())) { PsiExpression[] args = methodCall.getArgumentList().getExpressions(); if(args.length == 1) { listExpression = args[0]; } else if(args.length == 2) { listExpression = args[0]; comparatorExpression = args[1]; } else return null; } else if(InheritanceUtil.isInheritor(containingClass, CommonClassNames.JAVA_UTIL_LIST)) { listExpression = methodExpression.getQualifierExpression(); PsiExpression[] args = methodCall.getArgumentList().getExpressions(); if(args.length != 1) return null; comparatorExpression = args[0]; } if(!(listExpression instanceof PsiReferenceExpression) || !((PsiReferenceExpression)listExpression).isReferenceTo(list)) return null; if(comparatorExpression == null || ExpressionUtils.isNullLiteral(comparatorExpression)) return ""; return comparatorExpression.getText(); } @Nullable static PsiMethodCallExpression extractToArrayExpression(PsiLoopStatement statement, PsiMethodCallExpression expression) { // return collection.toArray() or collection.toArray(new Type[0]) or collection.toArray(new Type[collection.size()]); PsiElement nextElement = PsiTreeUtil.skipSiblingsForward(statement, PsiComment.class, PsiWhiteSpace.class); PsiExpression toArrayCandidate; if (nextElement instanceof PsiReturnStatement) { toArrayCandidate = ((PsiReturnStatement)nextElement).getReturnValue(); } else { PsiAssignmentExpression assignment = ExpressionUtils.getAssignment(nextElement); if (assignment != null) { toArrayCandidate = assignment.getRExpression(); } else if (nextElement instanceof PsiDeclarationStatement) { PsiElement[] elements = ((PsiDeclarationStatement)nextElement).getDeclaredElements(); if (elements.length == 1 && elements[0] instanceof PsiLocalVariable) { toArrayCandidate = ((PsiLocalVariable)elements[0]).getInitializer(); } else { return null; } } else { return null; } } if (!(toArrayCandidate instanceof PsiMethodCallExpression)) return null; PsiMethodCallExpression call = (PsiMethodCallExpression)toArrayCandidate; PsiReferenceExpression methodExpression = call.getMethodExpression(); if (!"toArray".equals(methodExpression.getReferenceName())) return null; PsiExpression qualifierExpression = methodExpression.getQualifierExpression(); if (!(qualifierExpression instanceof PsiReferenceExpression)) return null; PsiLocalVariable collectionVariable = extractCollectionVariable(expression.getMethodExpression().getQualifierExpression()); if (collectionVariable == null || !((PsiReferenceExpression)qualifierExpression).isReferenceTo(collectionVariable)) return null; PsiExpression initializer = collectionVariable.getInitializer(); if (initializer == null) return null; PsiType type = initializer.getType(); if (!(type instanceof PsiClassType) || !COLLECTION_TO_ARRAY.containsKey(((PsiClassType)type).rawType().getCanonicalText())) { return null; } if (!(nextElement instanceof PsiReturnStatement) && !ReferencesSearch.search(collectionVariable, collectionVariable.getUseScope()) .forEach(ref -> ref.getElement() == collectionVariable || PsiTreeUtil.isAncestor(statement, ref.getElement(), false) || PsiTreeUtil.isAncestor(toArrayCandidate, ref.getElement(), false) )) { return null; } PsiExpression[] args = call.getArgumentList().getExpressions(); if (args.length == 0) return call; if (args.length != 1 || !(args[0] instanceof PsiNewExpression)) return null; PsiNewExpression newArray = (PsiNewExpression)args[0]; PsiExpression[] dimensions = newArray.getArrayDimensions(); if (dimensions.length != 1) return null; if (ExpressionUtils.isLiteral(dimensions[0], 0)) return call; if (!(dimensions[0] instanceof PsiMethodCallExpression)) return null; PsiMethodCallExpression maybeSizeCall = (PsiMethodCallExpression)dimensions[0]; if (maybeSizeCall.getArgumentList().getExpressions().length != 0) return null; PsiReferenceExpression maybeSizeExpression = maybeSizeCall.getMethodExpression(); PsiExpression sizeQualifier = maybeSizeExpression.getQualifierExpression(); if (sizeQualifier != null && !("size".equals(maybeSizeExpression.getReferenceName()) && PsiEquivalenceUtil.areElementsEquivalent(qualifierExpression, sizeQualifier))) { return null; } return call; } /** * Intermediate stream operation representation */ static abstract class Operation { final PsiExpression myExpression; final PsiVariable myVariable; final @Nullable Operation myPreviousOp; protected Operation(@Nullable Operation previousOp, PsiExpression expression, PsiVariable variable) { myExpression = expression; myVariable = variable; myPreviousOp = previousOp; } public PsiVariable getVariable() { return myVariable; } @Nullable public Operation getPreviousOp() { return myPreviousOp; } PsiExpression getExpression() { return myExpression; } StreamEx expressions() { return StreamEx.of(myExpression); } abstract String createReplacement(); boolean isWriteAllowed(PsiVariable variable, PsiExpression reference) { return false; } } static class FilterOp extends Operation { private final boolean myNegated; FilterOp(@Nullable Operation previousOp, PsiExpression condition, PsiVariable variable, boolean negated) { super(previousOp, condition, variable); myNegated = negated; } public boolean isNegated() { return myNegated; } @Override public String createReplacement() { PsiElementFactory factory = JavaPsiFacade.getElementFactory(myExpression.getProject()); PsiExpression intermediate = makeIntermediateExpression(factory); PsiExpression expression = myNegated ? factory.createExpressionFromText(BoolUtils.getNegatedExpressionText(intermediate), myExpression) : intermediate; return ".filter(" + LambdaUtil.createLambda(myVariable, expression) + ")"; } PsiExpression makeIntermediateExpression(PsiElementFactory factory) { return myExpression; } } static class CompoundFilterOp extends FilterOp { private final FlatMapOp myFlatMapOp; private final PsiVariable myMatchVariable; CompoundFilterOp(FilterOp source, FlatMapOp flatMapOp) { super(flatMapOp.myPreviousOp, source.getExpression(), flatMapOp.myVariable, source.myNegated); myMatchVariable = source.myVariable; myFlatMapOp = flatMapOp; } @Override PsiExpression makeIntermediateExpression(PsiElementFactory factory) { return factory.createExpressionFromText(myFlatMapOp.getStreamExpression()+".anyMatch("+ LambdaUtil.createLambda(myMatchVariable, myExpression)+")", myExpression); } @Override StreamEx expressions() { return StreamEx.of(myExpression, myFlatMapOp.myExpression); } } static class MapOp extends Operation { private final @Nullable PsiType myType; MapOp(@Nullable Operation previousOp, PsiExpression expression, PsiVariable variable, @Nullable PsiType targetType) { super(previousOp, expression, variable); myType = targetType; } @Override public String createReplacement() { if (isIdentityMapping(myVariable, myExpression)) { if (!(myType instanceof PsiPrimitiveType)) { return myVariable.getType() instanceof PsiPrimitiveType ? ".boxed()" : ""; } if(myType.equals(myVariable.getType())) { return ""; } if (PsiType.LONG.equals(myType) && PsiType.INT.equals(myVariable.getType())) { return ".asLongStream()"; } if (PsiType.DOUBLE.equals(myType) && (PsiType.LONG.equals(myVariable.getType()) || PsiType.INT.equals(myVariable.getType()))) { return ".asDoubleStream()"; } } String operationName = "map"; if(myType instanceof PsiPrimitiveType) { if(!myType.equals(myVariable.getType())) { if(PsiType.INT.equals(myType)) { operationName = "mapToInt"; } else if(PsiType.LONG.equals(myType)) { operationName = "mapToLong"; } else if(PsiType.DOUBLE.equals(myType)) { operationName = "mapToDouble"; } } } else if(myVariable.getType() instanceof PsiPrimitiveType) { operationName = "mapToObj"; } PsiExpression expression = myType == null ? myExpression : RefactoringUtil.convertInitializerToNormalExpression(myExpression, myType); if(myType != null && !(myType instanceof PsiPrimitiveType) && !(myType instanceof PsiCapturedWildcardType)) { operationName = "<"+myType.getCanonicalText()+">"+operationName; } return "." + operationName + "(" + LambdaUtil.createLambda(myVariable, expression) + ")"; } @Override boolean isWriteAllowed(PsiVariable variable, PsiExpression reference) { return variable == myVariable && reference.getParent() == myExpression.getParent(); } } static class FlatMapOp extends Operation { private final PsiLoopStatement myLoop; private final StreamSource mySource; FlatMapOp(@Nullable Operation previousOp, StreamSource source, PsiVariable variable, PsiLoopStatement loop) { super(previousOp, source.getExpression(), variable); myLoop = loop; mySource = source; } @Override public String createReplacement() { String operation = "flatMap"; PsiType type = mySource.getVariable().getType(); if(type instanceof PsiPrimitiveType && !type.equals(myVariable.getType())) { if(type.equals(PsiType.INT)) { operation = "flatMapToInt"; } else if(type.equals(PsiType.LONG)) { operation = "flatMapToLong"; } else if(type.equals(PsiType.DOUBLE)) { operation = "flatMapToDouble"; } } return "." + operation + "(" + myVariable.getName() + " -> " + getStreamExpression() + ")"; } @NotNull String getStreamExpression() { return mySource.createReplacement(); } @Override boolean isWriteAllowed(PsiVariable variable, PsiExpression reference) { return mySource.isWriteAllowed(variable, reference); } boolean breaksMe(PsiBreakStatement statement) { return statement.findExitedStatement() == myLoop; } } abstract static class StreamSource extends Operation { protected StreamSource(PsiVariable variable, PsiExpression expression) { super(null, expression, variable); } void cleanUpSource() { } @Contract("null -> null") static StreamSource tryCreate(PsiLoopStatement statement) { if(statement instanceof PsiForStatement) { return CountingLoop.from((PsiForStatement)statement); } if(statement instanceof PsiForeachStatement) { ArrayStream source = ArrayStream.from((PsiForeachStatement)statement); return source == null ? CollectionStream.from((PsiForeachStatement)statement) : source; } if(statement instanceof PsiWhileStatement) { return BufferedReaderLines.from((PsiWhileStatement)statement); } return null; } } static class BufferedReaderLines extends StreamSource { private BufferedReaderLines(PsiVariable variable, PsiExpression expression) { super(variable, expression); } @Override String createReplacement() { return myExpression.getText()+".lines()"; } @Override void cleanUpSource() { myVariable.delete(); } @Override boolean isWriteAllowed(PsiVariable variable, PsiExpression reference) { return myVariable == variable && reference.getParent() == PsiTreeUtil.getParentOfType(myExpression, PsiAssignmentExpression.class); } @Nullable public static BufferedReaderLines from(PsiWhileStatement whileLoop) { // while ((line = br.readLine()) != null) PsiExpression condition = PsiUtil.skipParenthesizedExprDown(whileLoop.getCondition()); if(!(condition instanceof PsiBinaryExpression)) return null; PsiBinaryExpression binOp = (PsiBinaryExpression)condition; if(!JavaTokenType.NE.equals(binOp.getOperationTokenType())) return null; PsiExpression operand = null; if(ExpressionUtils.isNullLiteral(binOp.getROperand())) { operand = binOp.getLOperand(); } else if(ExpressionUtils.isNullLiteral(binOp.getLOperand())) { operand = binOp.getROperand(); } if(operand == null) return null; PsiAssignmentExpression assignment = ExpressionUtils.getAssignment(PsiUtil.skipParenthesizedExprDown(operand)); if(assignment == null) return null; PsiExpression lValue = assignment.getLExpression(); if(!(lValue instanceof PsiReferenceExpression)) return null; PsiElement element = ((PsiReferenceExpression)lValue).resolve(); if(!(element instanceof PsiLocalVariable)) return null; PsiLocalVariable var = (PsiLocalVariable)element; if(!ReferencesSearch.search(var, var.getUseScope()).forEach(ref -> { return PsiTreeUtil.isAncestor(whileLoop, ref.getElement(), true); })) { return null; } PsiExpression rValue = PsiUtil.skipParenthesizedExprDown(assignment.getRExpression()); if(!(rValue instanceof PsiMethodCallExpression)) return null; PsiMethodCallExpression call = (PsiMethodCallExpression)rValue; if(call.getArgumentList().getExpressions().length != 0) return null; if(!"readLine".equals(call.getMethodExpression().getReferenceName())) return null; PsiExpression readerExpression = call.getMethodExpression().getQualifierExpression(); if(readerExpression == null) return null; PsiMethod method = call.resolveMethod(); if(method == null) return null; PsiClass aClass = method.getContainingClass(); if(aClass == null || !"java.io.BufferedReader".equals(aClass.getQualifiedName())) return null; return new BufferedReaderLines(var, readerExpression); } } static class ArrayStream extends StreamSource { private ArrayStream(PsiVariable variable, PsiExpression expression) { super(variable, expression); } @Override String createReplacement() { return "java.util.Arrays.stream("+myExpression.getText() + ")"; } @Nullable public static ArrayStream from(PsiForeachStatement statement) { PsiExpression iteratedValue = statement.getIteratedValue(); if (iteratedValue == null) return null; PsiType iteratedValueType = iteratedValue.getType(); PsiParameter parameter = statement.getIterationParameter(); if (!(iteratedValueType instanceof PsiArrayType) || !isSupported(((PsiArrayType)iteratedValueType).getComponentType()) || ((parameter.getType() instanceof PsiPrimitiveType) && !parameter.getType().equals(((PsiArrayType)iteratedValueType).getComponentType()))) { return null; } return new ArrayStream(parameter, iteratedValue); } } static class CollectionStream extends StreamSource { private CollectionStream(PsiVariable variable, PsiExpression expression) { super(variable, expression); } @Override String createReplacement() { return ParenthesesUtils.getText(myExpression, ParenthesesUtils.POSTFIX_PRECEDENCE) + ".stream()" + tryUnbox(myVariable); } @Contract("null, _ -> false") static boolean isRawSubstitution(PsiType iteratedValueType, PsiClass collectionClass) { return iteratedValueType instanceof PsiClassType && PsiUtil .isRawSubstitutor(collectionClass, TypeConversionUtil.getSuperClassSubstitutor(collectionClass, (PsiClassType)iteratedValueType)); } @Nullable public static CollectionStream from(PsiForeachStatement statement) { PsiExpression iteratedValue = statement.getIteratedValue(); if (iteratedValue == null) return null; PsiType iteratedValueType = iteratedValue.getType(); PsiClass collectionClass = JavaPsiFacade.getInstance(statement.getProject()).findClass(CommonClassNames.JAVA_UTIL_COLLECTION, statement.getResolveScope()); PsiClass iteratorClass = PsiUtil.resolveClassInClassTypeOnly(iteratedValueType); if (collectionClass == null || !InheritanceUtil.isInheritorOrSelf(iteratorClass, collectionClass, true) || isRawSubstitution(iteratedValueType, collectionClass) || !isSupported(statement.getIterationParameter().getType())) { return null; } return new CollectionStream(statement.getIterationParameter(), iteratedValue); } } static class CountingLoop extends StreamSource { final PsiExpression myBound; final boolean myIncluding; private CountingLoop(PsiLocalVariable counter, PsiExpression initializer, PsiExpression bound, boolean including) { super(counter, initializer); myBound = bound; myIncluding = including; } @Override StreamEx expressions() { return StreamEx.of(myExpression, myBound); } @Override public String createReplacement() { String className = myVariable.getType().equals(PsiType.LONG) ? "java.util.stream.LongStream" : "java.util.stream.IntStream"; String methodName = myIncluding ? "rangeClosed" : "range"; return className+"."+methodName+"("+myExpression.getText()+", "+myBound.getText()+")"; } @Override boolean isWriteAllowed(PsiVariable variable, PsiExpression reference) { if(variable == myVariable) { PsiForStatement forStatement = PsiTreeUtil.getParentOfType(variable, PsiForStatement.class); if(forStatement != null) { return PsiTreeUtil.isAncestor(forStatement.getUpdate(), reference, false); } } return false; } @Nullable public static CountingLoop from(PsiForStatement forStatement) { // check that initialization is for(int/long i = ;...;...) if(!(forStatement.getInitialization() instanceof PsiDeclarationStatement)) return null; PsiDeclarationStatement initialization = (PsiDeclarationStatement)forStatement.getInitialization(); if(initialization.getDeclaredElements().length != 1) return null; PsiElement declaration = initialization.getDeclaredElements()[0]; if(!(declaration instanceof PsiLocalVariable)) return null; PsiLocalVariable counter = (PsiLocalVariable)declaration; if(!counter.getType().equals(PsiType.INT) && !counter.getType().equals(PsiType.LONG)) return null; PsiExpression initializer = counter.getInitializer(); if(initializer == null) return null; // check that increment is like for(...;...;i++) if(!(forStatement.getUpdate() instanceof PsiExpressionStatement)) return null; PsiExpression lValue = extractIncrementedLValue(((PsiExpressionStatement)forStatement.getUpdate()).getExpression()); if(!(lValue instanceof PsiReferenceExpression) || !((PsiReferenceExpression)lValue).isReferenceTo(counter)) return null; // check that condition is like for(...;i T getSingleExpression(Class wantedType) { PsiStatement statement = getSingleStatement(); if(statement instanceof PsiExpressionStatement) { PsiExpression expression = ((PsiExpressionStatement)statement).getExpression(); if(wantedType.isInstance(expression)) return wantedType.cast(expression); } return null; } /** * @return PsiMethodCallExpression if this TerminalBlock contains single method call, null otherwise */ @Nullable PsiMethodCallExpression getSingleMethodCall() { return getSingleExpression(PsiMethodCallExpression.class); } @Nullable private TerminalBlock extractFilter() { if(getSingleStatement() instanceof PsiIfStatement) { PsiIfStatement ifStatement = (PsiIfStatement)getSingleStatement(); if(ifStatement.getElseBranch() == null && ifStatement.getCondition() != null) { return new TerminalBlock(new FilterOp(myPreviousOp, ifStatement.getCondition(), myVariable, false), myVariable, ifStatement.getThenBranch()); } } if(myStatements.length >= 1) { PsiStatement first = myStatements[0]; // extract filter with negation if(first instanceof PsiIfStatement) { PsiIfStatement ifStatement = (PsiIfStatement)first; if(ifStatement.getCondition() == null) return null; PsiStatement branch = ifStatement.getThenBranch(); if(branch instanceof PsiBlockStatement) { PsiStatement[] statements = ((PsiBlockStatement)branch).getCodeBlock().getStatements(); if(statements.length == 1) branch = statements[0]; } if(!(branch instanceof PsiContinueStatement) || ((PsiContinueStatement)branch).getLabelIdentifier() != null) return null; PsiStatement[] statements; if(ifStatement.getElseBranch() != null) { statements = myStatements.clone(); statements[0] = ifStatement.getElseBranch(); } else { statements = Arrays.copyOfRange(myStatements, 1, myStatements.length); } return new TerminalBlock(new FilterOp(myPreviousOp, ifStatement.getCondition(), myVariable, true), myVariable, statements); } } return null; } /** * Returns an equivalent {@code TerminalBlock} with one more intermediate operation extracted * or null if extraction is not possible. * * @return extracted operation or null if extraction is not possible */ @Nullable TerminalBlock extractOperation() { TerminalBlock withFilter = extractFilter(); if(withFilter != null) return withFilter; // extract flatMap if(getSingleStatement() instanceof PsiLoopStatement) { PsiLoopStatement loopStatement = (PsiLoopStatement)getSingleStatement(); StreamSource source = StreamSource.tryCreate(loopStatement); final PsiStatement body = loopStatement.getBody(); if(source == null || body == null) return null; // flatMap from primitive to primitive is supported only if primitive types match // otherwise it would be necessary to create bogus step like // .mapToObj(var -> collection.stream()).flatMap(Function.identity()) if(myVariable.getType() instanceof PsiPrimitiveType && !myVariable.getType().equals(source.getVariable().getType())) return null; FlatMapOp op = new FlatMapOp(myPreviousOp, source, myVariable, loopStatement); TerminalBlock withFlatMap = new TerminalBlock(op, source.getVariable(), body); if(ReferencesSearch.search(myVariable, new LocalSearchScope(body)).findFirst() == null) { return withFlatMap; } else { // Try extract nested filter like this: // for(List subList : list) for(T t : subList) if(condition.test(t)) { ...; break; } // if t is not used in "...", then this could be converted to // list.stream().filter(subList -> subList.stream().anyMatch(condition)).forEach(subList -> ...) TerminalBlock withFlatMapFilter = withFlatMap.extractFilter(); if(withFlatMapFilter != null && !withFlatMapFilter.isEmpty()) { PsiStatement[] statements = withFlatMapFilter.getStatements(); PsiStatement lastStatement = statements[statements.length-1]; if (lastStatement instanceof PsiBreakStatement && op.breaksMe((PsiBreakStatement)lastStatement) && ReferencesSearch.search(withFlatMapFilter.getVariable(), new LocalSearchScope(statements)).findFirst() == null) { return new TerminalBlock(new CompoundFilterOp((FilterOp)withFlatMapFilter.getLastOperation(), op), myVariable, Arrays.copyOfRange(statements, 0, statements.length-1)); } } } } if(myStatements.length >= 1) { PsiStatement first = myStatements[0]; // extract map if(first instanceof PsiDeclarationStatement) { PsiDeclarationStatement decl = (PsiDeclarationStatement)first; PsiElement[] elements = decl.getDeclaredElements(); if(elements.length == 1) { PsiElement element = elements[0]; if(element instanceof PsiLocalVariable) { PsiLocalVariable declaredVar = (PsiLocalVariable)element; if(isSupported(declaredVar.getType())) { PsiExpression initializer = declaredVar.getInitializer(); PsiStatement[] leftOver = Arrays.copyOfRange(myStatements, 1, myStatements.length); if (initializer != null && ReferencesSearch.search(myVariable, new LocalSearchScope(leftOver)).findFirst() == null) { MapOp op = new MapOp(myPreviousOp, initializer, myVariable, declaredVar.getType()); return new TerminalBlock(op, declaredVar, leftOver); } } } } } PsiAssignmentExpression assignment = ExpressionUtils.getAssignment(first); if(assignment != null) { PsiExpression lValue = assignment.getLExpression(); PsiExpression rValue = assignment.getRExpression(); if(rValue != null && lValue instanceof PsiReferenceExpression && ((PsiReferenceExpression)lValue).isReferenceTo(myVariable)) { PsiStatement[] leftOver = Arrays.copyOfRange(myStatements, 1, myStatements.length); MapOp op = new MapOp(myPreviousOp, rValue, myVariable, myVariable.getType()); return new TerminalBlock(op, myVariable, leftOver); } } } return null; } @NotNull public Operation getLastOperation() { return myPreviousOp; } /** * Extract all possible intermediate operations * @return the terminal block with all possible terminal operations extracted (may return this if no operations could be extracted) */ @NotNull TerminalBlock extractOperations() { return StreamEx.iterate(this, Objects::nonNull, TerminalBlock::extractOperation).reduce((a, b) -> b).orElse(this); } @NotNull public PsiVariable getVariable() { return myVariable; } public boolean hasOperations() { return !(myPreviousOp instanceof StreamSource); } public boolean isEmpty() { return myStatements.length == 0; } @NotNull StreamEx operations() { return StreamEx.iterate(myPreviousOp, Objects::nonNull, Operation::getPreviousOp); } public Collection getOperations() { ArrayDeque ops = new ArrayDeque<>(); operations().forEach(ops::addFirst); return ops; } /** * @return stream of physical expressions used in intermediate operations in arbitrary order */ public StreamEx intermediateExpressions() { return operations().remove(StreamSource.class::isInstance).flatMap(Operation::expressions); } /** * @return stream of physical expressions used in stream source and intermediate operations in arbitrary order */ public StreamEx intermediateAndSourceExpressions() { return operations().flatMap(Operation::expressions); } /** * Converts this TerminalBlock to PsiElement (either PsiStatement or PsiCodeBlock) * * @param factory factory to use to create new element if necessary * @return the PsiElement */ public PsiElement convertToElement(PsiElementFactory factory) { if (myStatements.length == 1) { return myStatements[0]; } PsiCodeBlock block = factory.createCodeBlock(); for (PsiStatement statement : myStatements) { block.add(statement); } return block; } @NotNull public static TerminalBlock from(StreamSource source, PsiStatement body) { return new TerminalBlock(source, source.myVariable, body).extractOperations(); } } }