From fa416812fc74101af7a27150e70a9d2a2ad9aa6b Mon Sep 17 00:00:00 2001 From: Roman Ivanov Date: Wed, 2 Aug 2017 17:27:50 +0700 Subject: [PATCH] improve conversion to stream api for joining cases --- .../impl/quickfix/AddMethodQualifierFix.java | 3 + .../streamMigration/CollectMigration.java | 242 +--- .../streamMigration/JoiningMigration.java | 1287 +++++++++++++++++ .../SpecialFirstIterationLoop.java | 329 +++++ .../StreamApiMigrationInspection.java | 11 +- .../{collect => joining}/afterJoining.java | 0 .../afterJoiningAppendNotJustBeforeLoop.java | 11 + .../afterJoiningAssign.java | 0 .../joining/afterJoiningCharArray.java | 13 + .../joining/afterJoiningCharAt.java | 13 + .../joining/afterJoiningComplexAppend.java | 13 + .../afterJoiningConcat.java | 0 .../joining/afterJoiningConstants.java | 14 + .../afterJoiningDelimited.java | 0 .../joining/afterJoiningDelimitedBool.java | 19 + .../afterJoiningDelimitedBoolCommon.java | 19 + .../afterJoiningDelimitedBoolFlippedIf.java | 19 + .../afterJoiningDelimitedBoolNotFirst.java | 19 + .../afterJoiningDelimitedBoolPrefix.java | 19 + .../afterJoiningDelimitedConstant.java | 20 + .../afterJoiningDelimitedTruncated.java | 16 + .../joining/afterJoiningDelimiterRewrite.java | 11 + .../joining/afterJoiningFinalSb.java | 11 + .../afterJoiningInBrackets.java | 0 .../afterJoiningIndex.java} | 0 .../joining/afterJoiningIndexEqZero.java | 11 + .../joining/afterJoiningIndexFromSecond.java | 11 + .../afterJoiningIndexFromSecondPrefix.java | 11 + .../joining/afterJoiningIndexNotEq.java | 11 + .../joining/afterJoiningIndexNotEqElse.java | 11 + .../afterJoiningInitialValue.java | 0 .../joining/afterJoiningMixed.java | 13 + .../joining/afterJoiningMultipleAppend.java | 13 + .../afterJoiningReused.java | 0 .../afterJoiningStringBuilderReused.java | 0 .../joining/afterJoiningTernary.java | 12 + .../{collect => joining}/beforeJoining.java | 0 .../beforeJoiningAppendNotJustBeforeLoop.java | 18 + .../beforeJoiningAssign.java | 0 .../joining/beforeJoiningBuilderInSuffix.java | 17 + .../joining/beforeJoiningCharArray.java | 16 + .../joining/beforeJoiningCharAt.java | 16 + .../joining/beforeJoiningComplexAppend.java | 16 + .../beforeJoiningConcat.java | 0 .../joining/beforeJoiningConstants.java | 17 + .../beforeJoiningDelimited.java | 0 ...beforeJoiningDelimitedBadTruncateSize.java | 23 + .../joining/beforeJoiningDelimitedBool.java | 28 + .../beforeJoiningDelimitedBoolCommon.java | 29 + ...oiningDelimitedBoolDifferentJoinParts.java | 28 + .../beforeJoiningDelimitedBoolFlippedIf.java | 29 + .../beforeJoiningDelimitedBoolNotChanged.java | 27 + .../beforeJoiningDelimitedBoolNotFirst.java | 28 + .../beforeJoiningDelimitedBoolPrefix.java | 29 + .../beforeJoiningDelimitedConstant.java | 26 + .../beforeJoiningDelimitedTruncated.java | 23 + .../beforeJoiningDelimitedWrongBuilder.java | 0 .../beforeJoiningDelimiterRewrite.java | 18 + ...reJoiningDelimiterRewriteNonEmptyInit.java | 18 + .../joining/beforeJoiningFinalSb.java | 16 + .../beforeJoiningInBrackets.java | 2 +- .../beforeJoiningIndex.java} | 0 .../joining/beforeJoiningIndexEqZero.java | 17 + .../joining/beforeJoiningIndexFromSecond.java | 14 + .../beforeJoiningIndexFromSecondPrefix.java | 15 + .../joining/beforeJoiningIndexNotEq.java | 14 + .../joining/beforeJoiningIndexNotEqElse.java | 17 + .../beforeJoiningInitialValue.java | 0 .../joining/beforeJoiningLambda.java | 18 + .../joining/beforeJoiningLambdaAfterLoop.java | 16 + .../joining/beforeJoiningMixed.java | 16 + .../joining/beforeJoiningMultipleAppend.java | 14 + .../beforeJoiningReused.java | 0 .../beforeJoiningStringBuilderReused.java | 0 .../joining/beforeJoiningTernary.java | 13 + .../StreamApiMigrationInspectionTest.java | 8 + .../src/com/intellij/util/ObjectUtils.java | 1 + .../siyeh/ig/psiutils/ControlFlowUtils.java | 10 + .../siyeh/ig/psiutils/ExpressionUtils.java | 8 + 79 files changed, 2551 insertions(+), 236 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInspection/streamMigration/JoiningMigration.java create mode 100644 java/java-impl/src/com/intellij/codeInspection/streamMigration/SpecialFirstIterationLoop.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoining.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningAppendNotJustBeforeLoop.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoiningAssign.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharArray.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharAt.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningComplexAppend.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoiningConcat.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningConstants.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoiningDelimited.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBool.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolCommon.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolFlippedIf.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolNotFirst.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolPrefix.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedConstant.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedTruncated.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimiterRewrite.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningFinalSb.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoiningInBrackets.java (100%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect/afterJoiningUsingIndex.java => joining/afterJoiningIndex.java} (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexEqZero.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecond.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecondPrefix.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEq.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEqElse.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoiningInitialValue.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMixed.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMultipleAppend.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoiningReused.java (100%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/afterJoiningStringBuilderReused.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningTernary.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoining.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningAppendNotJustBeforeLoop.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningAssign.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningBuilderInSuffix.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharArray.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharAt.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningComplexAppend.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningConcat.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningConstants.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningDelimited.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBadTruncateSize.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBool.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolCommon.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolDifferentJoinParts.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolFlippedIf.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotChanged.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotFirst.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolPrefix.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedConstant.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedTruncated.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningDelimitedWrongBuilder.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewrite.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewriteNonEmptyInit.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningFinalSb.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningInBrackets.java (87%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect/beforeJoiningUsingIndex.java => joining/beforeJoiningIndex.java} (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexEqZero.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexFromSecond.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexFromSecondPrefix.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexNotEq.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexNotEqElse.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningInitialValue.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningLambda.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningLambdaAfterLoop.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMixed.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMultipleAppend.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningReused.java (100%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/{collect => joining}/beforeJoiningStringBuilderReused.java (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningTernary.java diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMethodQualifierFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMethodQualifierFix.java index fff6a40afa3c..317dae414994 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMethodQualifierFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMethodQualifierFix.java @@ -53,6 +53,9 @@ public class AddMethodQualifierFix implements IntentionAction { @Override public String getText() { if (myCandidates == null || myCandidates.isEmpty()) { + if(ApplicationManager.getApplication().isUnitTestMode()) { + return ""; + } throw new IllegalStateException(); } if (myCandidates.size() == 1) { diff --git a/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java b/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java index a751c69e9de2..18a34d8263a7 100644 --- a/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java +++ b/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java @@ -16,22 +16,18 @@ package com.intellij.codeInspection.streamMigration; import com.intellij.codeInsight.intention.impl.StreamRefactoringUtil; -import com.intellij.codeInspection.streamMigration.StreamApiMigrationInspection.CountingLoopSource; import com.intellij.codeInspection.util.LambdaGenerationUtil; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.codeStyle.JavaCodeStyleManager; 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.PsiTypesUtil; import com.intellij.psi.util.PsiUtil; import com.intellij.util.ArrayUtil; -import com.siyeh.ig.callMatcher.CallMatcher; import com.siyeh.ig.psiutils.*; import com.siyeh.ig.psiutils.ControlFlowUtils.InitializerUsageStatus; import one.util.streamex.EntryStream; @@ -82,7 +78,7 @@ class CollectMigration extends BaseStreamApiMigration { PsiElement migrate(@NotNull Project project, @NotNull PsiStatement body, @NotNull TerminalBlock tb) { PsiLoopStatement loopStatement = tb.getMainLoop(); PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); - CollectTerminal terminal = extractCollectTerminal(tb); + CollectTerminal terminal = extractCollectTerminal(tb, null); if (terminal == null) return null; String stream = tb.generate() + terminal.generateIntermediate() + terminal.generateTerminal(); PsiElement toReplace = terminal.getElementToReplace(); @@ -124,30 +120,21 @@ class CollectMigration extends BaseStreamApiMigration { } @Nullable - static CollectTerminal extractCollectTerminal(TerminalBlock tb) { + static CollectTerminal extractCollectTerminal(@NotNull TerminalBlock tb, @Nullable List nonFinalVariables) { + if(nonFinalVariables != null && !nonFinalVariables.isEmpty()) { + return null; + } + + PsiMethodCallExpression call; - PsiMethodCallExpression delimiterAppend; - PsiStatement[] statements = tb.getStatements(); - if (statements.length == 2) { - // Check for delimiter append like: if(sb.length() > 0) sb.append(", ") - if (!(statements[1] instanceof PsiExpressionStatement)) return null; - call = tryCast(((PsiExpressionStatement)statements[1]).getExpression(), PsiMethodCallExpression.class); - if (!(statements[0] instanceof PsiIfStatement)) return null; - delimiterAppend = StringBuilderTerminal.extractDelimiterAppend(tb, (PsiIfStatement)statements[0]); - if (delimiterAppend == null || VariableAccessUtils.variableIsUsed(tb.getVariable(), delimiterAppend)) return null; - } - else { - delimiterAppend = null; - call = tb.getSingleMethodCall(); - } + call = tb.getSingleMethodCall(); if (call == null) return null; PsiReferenceExpression methodExpression = call.getMethodExpression(); PsiExpression qualifierExpression = methodExpression.getQualifierExpression(); if (tb.dependsOn(qualifierExpression)) return null; List> extractors = Arrays - .asList(AddingTerminal::tryExtract, GroupingTerminal::tryExtract, ToMapTerminal::tryExtract, AddingAllTerminal::tryExtractAddAll, - (t, c) -> StringBuilderTerminal.tryExtract(t, c, delimiterAppend)); + .asList(AddingTerminal::tryExtract, GroupingTerminal::tryExtract, ToMapTerminal::tryExtract, AddingAllTerminal::tryExtractAddAll); CollectTerminal terminal = StreamEx.of(extractors).map(extractor -> extractor.apply(tb, call)).nonNull().findFirst().orElse(null); if (terminal != null) { @@ -535,217 +522,6 @@ class CollectMigration extends BaseStreamApiMigration { } } - static class StringBuilderTerminal extends CollectTerminal { - private static final CallMatcher APPEND = CallMatcher.anyOf( - CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_STRING_BUILDER, "append").parameterCount(1), - CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_STRING_BUFFER, "append").parameterCount(1) - ); - - private final PsiVariable myElement; - private final PsiMethodCallExpression myAppendCall; - private final PsiMethodCallExpression myFinalAppendCall; - private final PsiExpression myDelimiter; - - StringBuilderTerminal(PsiLocalVariable variable, - PsiLoopStatement loop, - PsiVariable element, - PsiMethodCallExpression appendCall, - PsiExpression delimiter, - PsiMethodCallExpression finalAppend) { - super(variable, loop, getInitializerUsageStatus(variable, loop)); - myElement = element; - myAppendCall = appendCall; - myFinalAppendCall = finalAppend; - myDelimiter = delimiter; - } - - @Override - public String generateIntermediate() { - PsiExpression mapping = myAppendCall.getArgumentList().getExpressions()[0]; - mapping = JavaPsiFacade.getElementFactory(mapping.getProject()).createExpressionFromText(expressionToCharSequence(mapping), mapping); - return StreamRefactoringUtil.generateMapOperation(myElement, null, mapping); - } - - @NotNull - private static String expressionToCharSequence(@NotNull PsiExpression expression) { - PsiType type = expression.getType(); - if (!InheritanceUtil.isInheritor(type, "java.lang.CharSequence")) { - if (expression instanceof PsiLiteralExpression) { - Object value = ((PsiLiteralExpression)expression).getValue(); - if (value instanceof Character) { - return "\"" + StringUtil.escapeStringCharacters(value.toString()) + "\""; - } - } - return CommonClassNames.JAVA_LANG_STRING + ".valueOf(" + expression.getText() + ")"; - } - return expression.getText(); - } - - @Override - String generateTerminal() { - String delimiter = myDelimiter == null ? "" : expressionToCharSequence(myDelimiter); - PsiExpression initializer = getTargetVariable().getInitializer(); - String initialText = ConstructionUtils.getStringBuilderInitializerText(initializer); - String finalText = "\"\""; - if (myFinalAppendCall != null) { - finalText = expressionToCharSequence(myFinalAppendCall.getArgumentList().getExpressions()[0]); - } - String args; - if ("\"\"".equals(initialText) && "\"\"".equals(finalText)) { - args = delimiter; - } - else { - args = (delimiter.isEmpty() ? "\"\"" : delimiter) + "," + initialText + "," + finalText; - } - return ".collect(" + CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS + ".joining(" + args + "))"; - } - - @Override - boolean isTrivial() { - return myDelimiter == null; - } - - @Override - void cleanUp() { - PsiLocalVariable target = getTargetVariable(); - PsiElementFactory factory = JavaPsiFacade.getElementFactory(target.getProject()); - target.getTypeElement().replace(factory.createTypeElementFromText(CommonClassNames.JAVA_LANG_STRING, target)); - if (getStatus() == ControlFlowUtils.InitializerUsageStatus.AT_WANTED_PLACE) { - PsiExpression initializer = target.getInitializer(); - String initialText = ConstructionUtils.getStringBuilderInitializerText(initializer); - if (initialText != null) { - initializer.replace(factory.createExpressionFromText(initialText, target)); - } - } - if (myFinalAppendCall != null) { - if (myFinalAppendCall.getParent() instanceof PsiExpressionStatement) { - myFinalAppendCall.delete(); - } else { - PsiMethodCallExpression nextCall = ExpressionUtils.getCallForQualifier(myFinalAppendCall); - PsiExpression qualifier = myFinalAppendCall.getMethodExpression().getQualifierExpression(); - if (nextCall != null && qualifier != null) { - nextCall.replace(qualifier); - } - } - } - Collection usages = ReferencesSearch.search(target).findAll(); - for (PsiReference usage : usages) { - PsiElement element = usage.getElement(); - if (element.isValid() && element instanceof PsiExpression) { - PsiMethodCallExpression call = ExpressionUtils.getCallForQualifier((PsiExpression)element); - if (call != null && "toString".equals(call.getMethodExpression().getReferenceName())) { - call.replace(element); - } - } - } - } - - static PsiMethodCallExpression getAfterLoopAppend(PsiLoopStatement loop, PsiVariable target) { - PsiElement next = PsiTreeUtil.skipWhitespacesAndCommentsForward(loop); - if (!(next instanceof PsiExpressionStatement)) return null; - PsiExpression expression = ((PsiExpressionStatement)next).getExpression(); - if (!(expression instanceof PsiMethodCallExpression)) return null; - PsiMethodCallExpression call = (PsiMethodCallExpression)expression; - if (APPEND.test(call) && ExpressionUtils.isReferenceTo(call.getMethodExpression().getQualifierExpression(), target)) return call; - return null; - } - - static PsiExpression getExpressionComparedToZero(PsiBinaryExpression condition) { - if (condition == null) return null; - IElementType tokenType = condition.getOperationTokenType(); - PsiExpression left = condition.getLOperand(); - PsiExpression right = condition.getROperand(); - if (ExpressionUtils.isZero(right)) { - if (tokenType.equals(JavaTokenType.NE) || tokenType.equals(JavaTokenType.GT)) return left; - } - else if (ExpressionUtils.isZero(left)) { - if (tokenType.equals(JavaTokenType.NE) || tokenType.equals(JavaTokenType.LT)) return right; - } - return null; - } - - static PsiMethodCallExpression extractDelimiterAppend(TerminalBlock tb, PsiIfStatement ifStatement) { - if (ifStatement.getElseBranch() != null) return null; - PsiExpressionStatement thenBranch = tryCast(ControlFlowUtils.stripBraces(ifStatement.getThenBranch()), PsiExpressionStatement.class); - if (thenBranch == null) return null; - PsiBinaryExpression condition = tryCast(PsiUtil.skipParenthesizedExprDown(ifStatement.getCondition()), PsiBinaryExpression.class); - PsiExpression comparedToZero = getExpressionComparedToZero(condition); - if (comparedToZero == null) return null; - PsiMethodCallExpression maybeLength = tryCast(PsiUtil.skipParenthesizedExprDown(comparedToZero), PsiMethodCallExpression.class); - PsiLocalVariable builder = null; - if (isCallOf(maybeLength, CommonClassNames.JAVA_LANG_ABSTRACT_STRING_BUILDER, "length")) { - builder = extractQualifierVariable(tb, maybeLength); - if (builder == null) return null; - } - else { - CountingLoopSource source = tb.getLastOperation(CountingLoopSource.class); - if (source == null || - !ExpressionUtils.isZero(source.getExpression()) || - !ExpressionUtils.isReferenceTo(comparedToZero, source.getVariable())) { - return null; - } - } - PsiMethodCallExpression call = tryCast(thenBranch.getExpression(), PsiMethodCallExpression.class); - if (!APPEND.test(call)) return null; - return builder == null || extractQualifierVariable(tb, call) == builder ? call : null; - } - - static StringBuilderTerminal tryExtract(TerminalBlock tb, PsiMethodCallExpression call, PsiMethodCallExpression delimiterAppend) { - if (tb.getCountExpression() != null) return null; - if (!APPEND.test(call)) return null; - PsiLocalVariable targetBuilder = extractQualifierVariable(tb, call); - if (targetBuilder == null) return null; - if (delimiterAppend != null && - !ExpressionUtils.isReferenceTo(delimiterAppend.getMethodExpression().getQualifierExpression(), targetBuilder)) { - return null; - } - String initialText = ConstructionUtils.getStringBuilderInitializerText(targetBuilder.getInitializer()); - if (initialText == null) return null; - PsiMethodCallExpression finalAppend = getAfterLoopAppend(tb.getMainLoop(), targetBuilder); - List refs = StreamEx.of(ReferencesSearch.search(targetBuilder).findAll()) - .map(PsiReference::getElement) - .remove(e -> PsiTreeUtil.isAncestor(targetBuilder, e, false) || PsiTreeUtil.isAncestor(tb.getMainLoop(), e, false)) - .toList(); - if (!refs.stream().allMatch(PsiExpression.class::isInstance)) return null; - boolean allowed = areReferencesAllowed(finalAppend, refs); - if (!allowed && refs.size() == 1 && finalAppend == null) { - PsiMethodCallExpression usage = ExpressionUtils.getCallForQualifier((PsiExpression)refs.get(0)); - if (APPEND.test(usage)) { - PsiMethodCallExpression nextCall = ExpressionUtils.getCallForQualifier(usage); - if (nextCall != null && "toString".equals(nextCall.getMethodExpression().getReferenceName())) { - finalAppend = usage; - allowed = true; - } - } - } - if (!allowed) return null; - PsiExpression delimiter = delimiterAppend == null ? null : delimiterAppend.getArgumentList().getExpressions()[0]; - return new StringBuilderTerminal(targetBuilder, tb.getMainLoop(), tb.getVariable(), call, delimiter, finalAppend); - } - - private static boolean areReferencesAllowed(PsiMethodCallExpression finalAppend, List refs) { - return StreamEx.of(refs).select(PsiExpression.class).allMatch(expression -> { - PsiMethodCallExpression usage = ExpressionUtils.getCallForQualifier(expression); - if (usage != null) { - if (usage == finalAppend) return true; - PsiExpression[] usageArgs = usage.getArgumentList().getExpressions(); - String name = usage.getMethodExpression().getReferenceName(); - if (usageArgs.length == 0 && ("toString".equals(name) || "length".equals(name))) return true; - } - PsiElement parent = PsiUtil.skipParenthesizedExprUp(expression.getParent()); - if (parent instanceof PsiPolyadicExpression && - ((PsiPolyadicExpression)parent).getOperationTokenType().equals(JavaTokenType.PLUS)) { - return true; - } - if (parent instanceof PsiAssignmentExpression && - ((PsiAssignmentExpression)parent).getOperationTokenType().equals(JavaTokenType.PLUSEQ)) { - return true; - } - return false; - }); - } - } - static class SortingTerminal extends CollectTerminal { private final CollectTerminal myDownstream; private final PsiExpression myComparator; diff --git a/java/java-impl/src/com/intellij/codeInspection/streamMigration/JoiningMigration.java b/java/java-impl/src/com/intellij/codeInspection/streamMigration/JoiningMigration.java new file mode 100644 index 000000000000..96d8ab64bc16 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/streamMigration/JoiningMigration.java @@ -0,0 +1,1287 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection.streamMigration; + + +import com.intellij.codeInsight.intention.impl.StreamRefactoringUtil; +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; +import com.intellij.codeInspection.dataFlow.value.DfaRelationValue; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.search.LocalSearchScope; +import com.intellij.psi.search.searches.ReferencesSearch; +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.HashSet; +import com.siyeh.ig.callMatcher.CallMatcher; +import com.siyeh.ig.psiutils.*; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import static com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil.isEffectivelyFinal; +import static com.intellij.codeInspection.streamMigration.StreamApiMigrationInspection.isCallOf; +import static com.intellij.util.ObjectUtils.tryCast; +import static com.siyeh.ig.psiutils.ControlFlowUtils.getInitializerUsageStatus; +import static com.siyeh.ig.psiutils.ExpressionUtils.resolveLocalVariable; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +public class JoiningMigration extends BaseStreamApiMigration { + protected JoiningMigration(boolean shouldWarn) { + super(shouldWarn, "collect"); + } + + @Override + PsiElement migrate(@NotNull Project project, @NotNull PsiStatement body, @NotNull TerminalBlock tb) { + JoiningTerminal terminal = extractTerminal(tb, null); + if(terminal == null) return null; + + TerminalBlock block = terminal.getTerminalBlock(); + PsiLoopStatement loopStatement = block.getMainLoop(); + String stream = terminal.generateStreamCode(); + restoreComments(loopStatement, body); + PsiLocalVariable builder = terminal.getBuilder(); + terminal.preCleanUp(); + ControlFlowUtils.InitializerUsageStatus status = getInitializerUsageStatus(builder, loopStatement); + PsiElement result = replaceInitializer(loopStatement, builder, builder.getInitializer(), stream, status); + terminal.cleanUp(builder); + JoiningTerminal.replaceUsages(terminal.getBuilder()); + return result; + } + + @Nullable + static JoiningTerminal extractTerminal(@NotNull TerminalBlock terminalBlock, + @Nullable("when fix applied") List nonFinalVariables) { + List, JoiningTerminal>> extractors = Arrays.asList( + JoiningTerminal.CountedLoopJoiningTerminal::extractCountedLoopTerminal, + JoiningTerminal.PlainJoiningTerminal::extractPlainJoining, + JoiningTerminal.LengthBasedJoiningTerminal::extractLengthBasedTerminal, + JoiningTerminal.BoolFlagJoiningTerminal::extractBoolFlagTerminal, + JoiningTerminal.LengthTruncateJoiningTerminal::extractLengthTruncateTerminal, + JoiningTerminal.DelimiterRewriteJoiningTerminal::extractDelimiterRewritingTerminal, + JoiningTerminal.IndexBasedJoiningTerminal::extractIndexBasedTerminal + ); + return StreamEx.of(extractors) + .map(extractor -> extractor.apply(terminalBlock, nonFinalVariables)) + .nonNull() + .findFirst() + .orElse(null); + } + + static class JoiningTerminal { + private static final CallMatcher APPEND = CallMatcher.anyOf( + CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_STRING_BUILDER, "append").parameterCount(1), + CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_STRING_BUFFER, "append").parameterCount(1) + ); + + + private static final CallMatcher LENGTH = + CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_ABSTRACT_STRING_BUILDER, "length").parameterCount(0); + private static final CallMatcher EMPTY_LENGTH = + CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_ABSTRACT_STRING_BUILDER, "isEmpty").parameterCount(0); + private static final CallMatcher SET_LENGTH = + CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_ABSTRACT_STRING_BUILDER, "setLength").parameterCount(1); + + + private static final EquivalenceChecker ourEquivalence = EquivalenceChecker.getCanonicalPsiEquivalence(); + + private final @NotNull TerminalBlock myTerminalBlock; + private final @NotNull PsiLocalVariable myBuilder; + private final @NotNull PsiVariable myLoopVariable; + private final @NotNull List myMainJoinParts; + private final @NotNull List myPrefixJoinParts; + private final @NotNull List mySuffixJoinParts; + private final @NotNull List myDelimiterJoinParts; + private final @Nullable PsiMethodCallExpression myBeforeLoopAppend; + private final @Nullable PsiMethodCallExpression myAfterLoopAppend; + + @NotNull + public TerminalBlock getTerminalBlock() { + return myTerminalBlock; + } + + @NotNull + public PsiLocalVariable getBuilder() { + return myBuilder; + } + + protected JoiningTerminal(@NotNull TerminalBlock block, + @NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull List prefixJoinParts, + @NotNull List suffixJoinParts, + @NotNull List delimiterJoinParts, + @Nullable PsiMethodCallExpression beforeLoopAppend, + @Nullable PsiMethodCallExpression afterLoopAppend) { + myTerminalBlock = block; + myBuilder = targetBuilder; + myLoopVariable = variable; + myMainJoinParts = mainJoinParts; + myPrefixJoinParts = prefixJoinParts; + mySuffixJoinParts = suffixJoinParts; + myDelimiterJoinParts = delimiterJoinParts; + myBeforeLoopAppend = beforeLoopAppend; + myAfterLoopAppend = afterLoopAppend; + } + + void cleanUp(@NotNull PsiLocalVariable target) { + replaceInitializer(target); + replaceUsages(target); + } + + void preCleanUp() { + cleanUpCall(myBeforeLoopAppend); + cleanUpCall(myAfterLoopAppend); + } + + @NotNull + String generateStreamCode() { + return myTerminalBlock.generate() + generateIntermediate() + generateTerminal(); + } + + private static void replaceInitializer(@NotNull PsiLocalVariable target) { + PsiElementFactory factory = JavaPsiFacade.getElementFactory(target.getProject()); + target.getTypeElement().replace(factory.createTypeElementFromText(CommonClassNames.JAVA_LANG_STRING, target)); + PsiExpression initializer = target.getInitializer(); + String initialText = ConstructionUtils.getStringBuilderInitializerText(initializer); + if (initialText != null) { + initializer.replace(factory.createExpressionFromText("\"\"", target)); + } + } + + private static boolean canBeMadeNonFinal(@NotNull PsiLocalVariable variable, @NotNull PsiLoopStatement loop) { + NavigatablePsiElement loopBound = PsiTreeUtil.getParentOfType(loop, PsiMember.class, PsiLambdaExpression.class); + return ReferencesSearch.search(variable) + .forEach(reference -> PsiTreeUtil.getParentOfType(reference.getElement(), PsiMember.class, PsiLambdaExpression.class) == loopBound); + } + + String generateTerminal() { + final String collectArguments; + if (myDelimiterJoinParts.isEmpty() && myPrefixJoinParts.isEmpty() && mySuffixJoinParts.isEmpty()) { + collectArguments = ""; + } + else { + String delimiter = myDelimiterJoinParts.isEmpty() ? "\"\"" : getExpressionText(myDelimiterJoinParts); + if (mySuffixJoinParts.isEmpty() && myPrefixJoinParts.isEmpty()) { + collectArguments = delimiter; + } + else { + String suffix = mySuffixJoinParts.isEmpty() ? "\"\"" : getExpressionText(mySuffixJoinParts); + String prefix = myPrefixJoinParts.isEmpty() ? "\"\"" : getExpressionText(myPrefixJoinParts); + collectArguments = delimiter + "," + prefix + "," + suffix; + } + } + return ".collect(" + CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS + ".joining(" + collectArguments + "))"; + } + + String generateIntermediate() { + PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(myLoopVariable.getProject()); + String joinTransformation = getExpressionText(myMainJoinParts); + PsiExpression mapping = elementFactory.createExpressionFromText(joinTransformation, myLoopVariable); + return StreamRefactoringUtil.generateMapOperation(myLoopVariable, null, mapping); + } + + + private static void replaceUsages(PsiLocalVariable target) { + Collection usages = ReferencesSearch.search(target).findAll(); + for (PsiReference usage : usages) { + PsiElement element = usage.getElement(); + if (element.isValid() && element instanceof PsiExpression) { + PsiMethodCallExpression call = ExpressionUtils.getCallForQualifier((PsiExpression)element); + if (call != null && "toString".equals(call.getMethodExpression().getReferenceName())) { + call.replace(element); + } + } + } + } + + private static void cleanUpCall(PsiMethodCallExpression call) { + if (call != null) { + if (call.getParent() instanceof PsiExpressionStatement) { + call.delete(); + } + else { + PsiMethodCallExpression nextCall = ExpressionUtils.getCallForQualifier(call); + PsiExpression qualifier = call.getMethodExpression().getQualifierExpression(); + if (nextCall != null && qualifier != null) { + nextCall.replace(qualifier); + } + } + } + } + + + private static String getExpressionText(@NotNull List joinParts) { + StringJoiner joiner = new StringJoiner("+"); + int size = joinParts.size(); + for (int i = 0; i < joinParts.size(); i++) { + PsiExpression joinPart = joinParts.get(i); + String partText; + if (i == 0) { + boolean neighborIsString = false; + if (joinParts.size() > 1) { + PsiExpression second = joinParts.get(1); + if (TypeUtils.isJavaLangString(second.getType())) { + neighborIsString = true; + } + } + partText = expressionToCharSequence(joinPart, size, neighborIsString); + } + else { + partText = expressionToCharSequence(joinPart, size, true); + } + joiner.add(partText); + } + return joiner.toString(); + } + + @Nullable + private static String computeConstant(@NotNull PsiExpression expression) { + Object constantExpression = ExpressionUtils.computeConstantExpression(expression); + if (constantExpression != null) { + return String.valueOf(constantExpression); + } else { + PsiLocalVariable variable = resolveLocalVariable(expression); + if(variable == null) return null; + PsiElement parent = variable.getParent(); + PsiExpression initializer = variable.getInitializer(); + if(parent == null || initializer == null) return null; + if(!isEffectivelyFinal(variable, parent, null)) return null; + Object initializerConstant = ExpressionUtils.computeConstantExpression(initializer); + if(initializerConstant == null) return null; + return String.valueOf(initializerConstant); + } + } + + @Nullable + private static String computeConstant(@NotNull List joinParts) { + StringBuilder sb = new StringBuilder(); + for (PsiExpression expression : joinParts) { + String constant = computeConstant(expression); + if(constant == null) return null; + sb.append(constant); + } + return sb.toString(); + } + + @NotNull + private static String expressionToCharSequence(@NotNull PsiExpression expression, int expressionCount, boolean neighborIsString) { + PsiType type = expression.getType(); + if(expression instanceof PsiMethodCallExpression) { + PsiMethodCallExpression callExpression = (PsiMethodCallExpression)expression; + if(isCallOf(callExpression, CommonClassNames.JAVA_LANG_STRING, "charAt")) { + PsiExpression qualifierExpression = callExpression.getMethodExpression().getQualifierExpression(); + PsiExpression[] expressions = callExpression.getArgumentList().getExpressions(); + if(expressions.length == 1) { + PsiExpression first = expressions[0]; + if(qualifierExpression != null) { + + Object constantExpression = ExpressionUtils.computeConstantExpression(first); + if(constantExpression instanceof Integer) { + String endIndex = String.valueOf((int)constantExpression + 1); + return qualifierExpression.getText() + ".substring(" + first.getText() + "," + endIndex + ")"; + } + } + } + } + } + if (!InheritanceUtil.isInheritor(type, "java.lang.CharSequence")) { + if (!neighborIsString || (type instanceof PsiArrayType && ((PsiArrayType)type).getComponentType().equals(PsiType.CHAR))) { + PsiLiteralExpression literalExpression = tryCast(expression, PsiLiteralExpression.class); + if (literalExpression != null) { + Object value = literalExpression.getValue(); + if (value instanceof Character) { + String text = literalExpression.getText(); + if ("'\"'".equals(text)) return "\"\\\"\""; + return "\"" + text.substring(1, text.length() - 1) + "\""; + } + } + return CommonClassNames.JAVA_LANG_STRING + ".valueOf(" + expression.getText() + ")"; + } + if (ParenthesesUtils.getPrecedence(expression) > ParenthesesUtils.ADDITIVE_PRECEDENCE || + (expression.getType() instanceof PsiPrimitiveType && + ParenthesesUtils.getPrecedence(expression) == ParenthesesUtils.ADDITIVE_PRECEDENCE) || + expressionCount == 1) { + return "(" + expression.getText() + ")"; + } + return expression.getText(); + } + String expressionText = expression.getText(); + if(ParenthesesUtils.getPrecedence(expression) > ParenthesesUtils.ADDITIVE_PRECEDENCE && expressionCount > 1) { + expressionText = "(" + expressionText + ")"; + } + return expressionText; + } + + /** + * from statement like sb.append(a).append(b) extracts sb + */ + // + @Nullable("when failed to extract") + private static PsiLocalVariable extractStringBuilder(@NotNull PsiStatement statement) { + PsiExpressionStatement expressionStatement = tryCast(statement, PsiExpressionStatement.class); + if (expressionStatement == null) return null; + PsiMethodCallExpression methodCallExpression = tryCast(expressionStatement.getExpression(), PsiMethodCallExpression.class); + if (methodCallExpression == null) return null; + PsiMethodCallExpression currentExpression = methodCallExpression; + while (APPEND.test(methodCallExpression)) { + PsiExpression qualifierExpression = currentExpression.getMethodExpression().getQualifierExpression(); + PsiMethodCallExpression callerExpression = MethodCallUtils.getQualifierMethodCall(currentExpression); + if (callerExpression == null) { + return resolveLocalVariable(qualifierExpression); + } + currentExpression = callerExpression; + } + return null; + } + + @Nullable("when failed to extract") + private static List extractJoinParts(@Nullable PsiExpression expression) { + List joinParts = new ArrayList<>(); + if (expression == null) return joinParts; + return !tryExtractJoinPart(expression, joinParts) ? null : joinParts; + } + + /** + * @param statements list of statements. Only appends expected inside. + * @return list of joining expressions + */ + @Nullable("when failed to extract") + private static List extractJoinParts(@NotNull List statements) { + List joinParts = new ArrayList<>(); + for (PsiStatement statement : statements) { + PsiExpressionStatement expressionStatement = tryCast(statement, PsiExpressionStatement.class); + if (expressionStatement == null) return null; + PsiExpression expression = expressionStatement.getExpression(); + if (!tryExtractJoinPart(expression, joinParts)) { + return null; + } + } + return joinParts; + } + + /** + * @param joinParts list to append joining parts into it + * @return true on success + */ + private static boolean tryExtractJoinPart(@NotNull PsiExpression expression, + @NotNull List joinParts) { + PsiMethodCallExpression methodCallExpression = tryCast(expression, PsiMethodCallExpression.class); + if (methodCallExpression != null) { + if (!APPEND.test(methodCallExpression)) return false; + PsiExpression appendArgument = methodCallExpression.getArgumentList().getExpressions()[0]; + PsiExpression qualifierExpression = methodCallExpression.getMethodExpression().getQualifierExpression(); + if (qualifierExpression == null) return false; + PsiReferenceExpression referenceExpression = tryCast(qualifierExpression, PsiReferenceExpression.class); + if (referenceExpression == null) { // assume expr like sb.append(a).append(b).append(c) + if (!tryExtractJoinPart(qualifierExpression, joinParts)) return false; + } + if (!tryExtractConcatenationParts(appendArgument, joinParts)) return false; + return true; + } + return false; + } + + private static boolean tryExtractConcatenationParts(@NotNull PsiExpression expression, @NotNull List joinParts) { + PsiType type = expression.getType(); + if (type == null) return false; + if (!TypeUtils.isJavaLangString(type)) { + joinParts.add(expression); + return true; + } + PsiBinaryExpression binaryExpression = tryCast(expression, PsiBinaryExpression.class); + if (binaryExpression != null) { + if (binaryExpression.getOperationTokenType().equals(JavaTokenType.PLUS)) { + PsiExpression lOperand = PsiUtil.skipParenthesizedExprDown(binaryExpression.getLOperand()); + PsiExpression rOperand = PsiUtil.skipParenthesizedExprDown(binaryExpression.getROperand()); + if (lOperand == null || rOperand == null) return false; + PsiType lOperandType = lOperand.getType(); + PsiType rOperandType = rOperand.getType(); + if (lOperandType == null || rOperandType == null) return false; + if (!tryExtractConcatenationParts(lOperand, joinParts) || + !tryExtractConcatenationParts(rOperand, joinParts)) { + return false; + } + return true; + } + } + joinParts.add(expression); + return true; + } + + @Nullable + private static PsiExpression extractStringBuilderInitializer(PsiExpression construction) { + PsiNewExpression newExpression = tryCast(PsiUtil.skipParenthesizedExprDown(construction), PsiNewExpression.class); + if (newExpression == null) return null; + final PsiJavaCodeReferenceElement classReference = newExpression.getClassReference(); + if (classReference == null) return null; + PsiClass aClass = tryCast(classReference.resolve(), PsiClass.class); + if (aClass == null) return null; + final String qualifiedName = aClass.getQualifiedName(); + if (!CommonClassNames.JAVA_LANG_STRING_BUILDER.equals(qualifiedName) && + !CommonClassNames.JAVA_LANG_STRING_BUFFER.equals(qualifiedName)) { + return null; + } + final PsiExpressionList argumentList = newExpression.getArgumentList(); + if (argumentList == null) return null; + final PsiExpression[] arguments = argumentList.getExpressions(); + if (arguments.length != 1) return null; + final PsiExpression argument = arguments[0]; + final PsiType argumentType = argument.getType(); + if (PsiType.INT.equals(argumentType)) return null; + return argument; + } + + @Nullable + private static PsiMethodCallExpression getCallAfterStatement(PsiStatement statement, PsiVariable receiver, CallMatcher callMatcher) { + PsiElement next = PsiTreeUtil.skipWhitespacesAndCommentsForward(statement); + return getCallExpression(receiver, callMatcher, next); + } + + @Nullable + private static PsiMethodCallExpression getCallBeforeStatement(@NotNull PsiStatement statement, + @NotNull PsiVariable receiver, + @NotNull CallMatcher callMatcher, + @NotNull List declarationsToSkip) { + PsiElement previous = PsiTreeUtil.skipWhitespacesAndCommentsBackward(statement); + PsiDeclarationStatement previousDeclaration = tryCast(previous, PsiDeclarationStatement.class); + while (previousDeclaration != null && declarationsToSkip.contains(previousDeclaration)) { + previous = PsiTreeUtil.skipWhitespacesAndCommentsBackward(previousDeclaration); + previousDeclaration = tryCast(previous, PsiDeclarationStatement.class); + } + return getCallExpression(receiver, callMatcher, previous); + } + + @Contract("_, _, null -> null") + @Nullable + private static PsiMethodCallExpression getCallExpression(@NotNull PsiVariable target, @NotNull CallMatcher callMatcher, @Nullable PsiElement element) { + if (!(element instanceof PsiExpressionStatement)) return null; + PsiExpression expression = ((PsiExpressionStatement)element).getExpression(); + if (!(expression instanceof PsiMethodCallExpression)) return null; + PsiMethodCallExpression call = (PsiMethodCallExpression)expression; + if (callMatcher.test(call) && ExpressionUtils.isReferenceTo(call.getMethodExpression().getQualifierExpression(), target)) return call; + return null; + } + + private static boolean areReferencesAllowed(@NotNull List refs, + @NotNull Set allowedReferencePlaces) { + return StreamEx.of(refs).select(PsiExpression.class).allMatch(expression -> { + PsiMethodCallExpression usage = ExpressionUtils.getCallForQualifier(expression); + if (usage != null) { + if (allowedReferencePlaces.contains(usage)) return true; + PsiExpression[] usageArgs = usage.getArgumentList().getExpressions(); + String name = usage.getMethodExpression().getReferenceName(); + if (usageArgs.length == 0 && ("toString".equals(name) || "length".equals(name))) return true; + } + PsiElement parent = PsiUtil.skipParenthesizedExprUp(expression.getParent()); + if (parent instanceof PsiPolyadicExpression && + ((PsiPolyadicExpression)parent).getOperationTokenType().equals(JavaTokenType.PLUS)) { + return true; + } + if (parent instanceof PsiAssignmentExpression && + ((PsiAssignmentExpression)parent).getOperationTokenType().equals(JavaTokenType.PLUSEQ)) { + return true; + } + return false; + }); + } + + @Nullable + private static PsiMethodCallExpression tryExtractCombinedToString(PsiMethodCallExpression afterLoopAppend, + List refs) { + if (refs.size() == 1 && afterLoopAppend == null) { // case like return sb.append(postfix).toString(); + PsiMethodCallExpression usage = ExpressionUtils.getCallForQualifier((PsiExpression)refs.get(0)); + if (APPEND.test(usage)) { + PsiMethodCallExpression nextCall = ExpressionUtils.getCallForQualifier(usage); + if (nextCall != null && "toString".equals(nextCall.getMethodExpression().getReferenceName())) { + return usage; + } + } + } + return null; + } + + + private static boolean joinPartsAreEquivalent(@NotNull List joinParts1, @NotNull List joinParts2) { + if (joinParts1.size() != joinParts2.size()) return false; + for (int i = 0, size = joinParts1.size(); i < size; i++) { + PsiExpression joinPart1 = joinParts1.get(i); + PsiExpression joinPart2 = joinParts2.get(i); + if (!ourEquivalence.expressionsAreEquivalent(joinPart1, joinPart2)) return false; + } + return true; + } + + /** + * Like: if(!sb.isEmpty()) => prefixLength == 0 or if(sb.length() > 2) => prefixLength == 2 + */ + @Nullable + private static Integer extractConditionPrefixLength(@NotNull PsiExpression expression, PsiLocalVariable targetBuilder) { + Integer explicitLengthCondition = extractExplicitLengthCheck(expression, targetBuilder); + if (explicitLengthCondition != null) return explicitLengthCondition; + return extractEmptyLengthCheck(expression, targetBuilder); + } + + @Nullable + private static Integer extractEmptyLengthCheck(@NotNull PsiExpression expression, PsiLocalVariable targetBuilder) { + PsiMethodCallExpression maybeEmptyCall = tryCast(BoolUtils.getNegated(expression), PsiMethodCallExpression.class); + if (!EMPTY_LENGTH.test(maybeEmptyCall)) return null; // extract call matcher + if (!ExpressionUtils.isReferenceTo(maybeEmptyCall.getMethodExpression().getQualifierExpression(), targetBuilder)) return null; + return 0; + } + + @Nullable("when failed to extract length") + private static Integer extractExplicitLengthCheck(@NotNull PsiExpression expression, PsiLocalVariable targetBuilder) { + PsiBinaryExpression condition = tryCast(expression, PsiBinaryExpression.class); + if (condition == null) return null; + PsiExpression rOperand = condition.getROperand(); + if (rOperand == null) return null; + + + PsiExpression lOperand = condition.getLOperand(); + DfaRelationValue.RelationType relation = DfaRelationValue.RelationType.fromElementType(condition.getOperationTokenType()); + if (relation == null) return null; + int lSize = computeConstantIntExpression(lOperand); + if (lSize >= 0) { + return extractLength(rOperand, relation.getFlipped(), lSize, targetBuilder); + } + else { + int rSize = computeConstantIntExpression(condition.getROperand()); + if (rSize >= 0) { + return extractLength(lOperand, relation, rSize, targetBuilder); + } + else { + return null; + } + } + } + + @Nullable + private static Integer extractLength(PsiExpression rOperand, + DfaRelationValue.RelationType relation, + int size, + PsiLocalVariable targetBuilder) { + if (!isStringBuilderLengthCall(rOperand, targetBuilder)) return null; + LongRangeSet rangeSet = LongRangeSet.point(size).fromRelation(relation); + if (rangeSet == null || rangeSet.max() != Long.MAX_VALUE) return null; + long min = rangeSet.min(); + return min > 0 ? (int)(min - 1) : null; + } + + private static boolean isStringBuilderLengthCall(@NotNull PsiExpression expression, PsiLocalVariable targetBuilder) { + PsiMethodCallExpression methodCallExpression = tryCast(expression, PsiMethodCallExpression.class); + return LENGTH.test(methodCallExpression) && + ExpressionUtils.isReferenceTo(methodCallExpression.getMethodExpression().getQualifierExpression(), targetBuilder); + } + + /** + * @param expression that is expected to be positive or 0 + * @return evaluated value or -1 when error + */ + private static int computeConstantIntExpression(@NotNull PsiExpression expression) { + Object constantExpression = ExpressionUtils.computeConstantExpression(expression); + if (!(constantExpression instanceof Integer)) return -1; + return (int)constantExpression; + } + + + private static class PrefixSuffixContext { + private final @Nullable PsiMethodCallExpression myBeforeLoopStatement; + private final @Nullable PsiMethodCallExpression myAfterLoopStatement; + private final @NotNull List myPrefixJoinParts; + private final @NotNull List mySuffixJoinParts; + + public PrefixSuffixContext(@Nullable PsiMethodCallExpression beforeLoopStatement, + @Nullable PsiMethodCallExpression afterLoopStatement, + @NotNull List prefixJoinParts, + @NotNull List suffixJoinParts) { + myBeforeLoopStatement = beforeLoopStatement; + myAfterLoopStatement = afterLoopStatement; + myPrefixJoinParts = prefixJoinParts; + mySuffixJoinParts = suffixJoinParts; + } + + @Nullable + public PsiMethodCallExpression getBeforeLoopStatement() { + return myBeforeLoopStatement; + } + + @Nullable + public PsiMethodCallExpression getAfterLoopStatement() { + return myAfterLoopStatement; + } + + @NotNull + public List getPrefixJoinParts() { + return myPrefixJoinParts; + } + + @NotNull + public List getSuffixJoinParts() { + return mySuffixJoinParts; + } + + /** + * @param finalAppendPredecessor - statement, after and which expected suffix append (loop statement generally) + * @param firstAppendSuccessor - statement before which expected prefix append and possibly some declarations used in loop + * @param targetBuilder - string builder used + * @param possibleVariablesBeforeLoop - variable, which declarations that can be before loop + * @param allowedReferencePlaces + * @return prefix and suffix data + */ + @Nullable + static PrefixSuffixContext extractAndVerifyRefs(@NotNull PsiStatement finalAppendPredecessor, + @NotNull PsiStatement firstAppendSuccessor, + @NotNull PsiLocalVariable targetBuilder, + @NotNull TerminalBlock terminalBlock, + @NotNull List possibleVariablesBeforeLoop, + @NotNull Set allowedReferencePlaces) { + PsiMethodCallExpression afterLoopAppend = getCallAfterStatement(finalAppendPredecessor, targetBuilder, APPEND); + + List declarations = getDeclarations(possibleVariablesBeforeLoop); + if(declarations == null) return null; + PsiMethodCallExpression beforeLoopAppend = getCallBeforeStatement(firstAppendSuccessor, targetBuilder, APPEND, declarations); + + if(!canBeMadeNonFinal(targetBuilder, terminalBlock.getMainLoop())) return null; + + List refs = StreamEx.of(ReferencesSearch.search(targetBuilder).findAll()) + .map(PsiReference::getElement) + .remove(e -> PsiTreeUtil.isAncestor(targetBuilder, e, false) || + PsiTreeUtil.isAncestor(terminalBlock.getMainLoop(), e, false)) + .toList(); + + allowedReferencePlaces.add(afterLoopAppend); + allowedReferencePlaces.add(beforeLoopAppend); + + boolean allowed = areReferencesAllowed(refs, allowedReferencePlaces); + if (!allowed) { + PsiMethodCallExpression newAfterLoopAppend = tryExtractCombinedToString(afterLoopAppend, refs); + if (newAfterLoopAppend == null) return null; + afterLoopAppend = newAfterLoopAppend; + } + PsiExpression builderStrInitializer = extractStringBuilderInitializer(targetBuilder.getInitializer()); + List prefixJoinParts = extractJoinParts(beforeLoopAppend); + if (prefixJoinParts == null) return null; + if (builderStrInitializer != null) { + prefixJoinParts.add(0, builderStrInitializer); + } + if (prefixJoinParts.stream().anyMatch(joinPart -> SideEffectChecker.mayHaveSideEffects(joinPart))) return null; + if (afterLoopAppend != null && VariableAccessUtils.variableIsUsed(targetBuilder, afterLoopAppend.getArgumentList())) return null; + List suffixJoinParts = extractJoinParts(afterLoopAppend); + if (suffixJoinParts == null) return null; + return new PrefixSuffixContext(beforeLoopAppend, afterLoopAppend, prefixJoinParts, suffixJoinParts); + } + + /** + * @return list of declaration statements or null if error + */ + @Nullable("when failed to get declaration of any var") + static List getDeclarations(@NotNull List variables) { + List list = new ArrayList<>(); + for (PsiLocalVariable var : variables) { + PsiDeclarationStatement declarationStatement = PsiTreeUtil.getParentOfType(var, PsiDeclarationStatement.class); + if(declarationStatement == null) return null; + list.add(declarationStatement); + } + return list; + } + } + + + /** + * Joining without delimiter, but maybe with prefix and suffix + */ + private static class PlainJoiningTerminal extends JoiningTerminal { + protected PlainJoiningTerminal(@NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull PrefixSuffixContext prefixSuffixContext, + @NotNull TerminalBlock block) { + super(block, targetBuilder, variable, mainJoinParts, prefixSuffixContext.getPrefixJoinParts(), + prefixSuffixContext.getSuffixJoinParts(), emptyList(), prefixSuffixContext.getBeforeLoopStatement(), + prefixSuffixContext.getAfterLoopStatement()); + } + + @Nullable + static PlainJoiningTerminal extractPlainJoining(@NotNull TerminalBlock terminalBlock, @Nullable List nonFinalVariables) { + if (nonFinalVariables != null && !nonFinalVariables.isEmpty()) return null; + List statements = Arrays.asList(terminalBlock.getStatements()); + List mainJoinParts = extractJoinParts(statements); + if (mainJoinParts == null || mainJoinParts.isEmpty()) return null; + PsiLocalVariable targetBuilder = extractStringBuilder(statements.get(0)); + if (targetBuilder == null) return null; + PsiLoopStatement loop = terminalBlock.getMainLoop(); + PrefixSuffixContext context = + PrefixSuffixContext.extractAndVerifyRefs(loop, loop, targetBuilder, terminalBlock, emptyList(), new HashSet<>(emptyList())); + if (context == null) return null; + return new PlainJoiningTerminal(targetBuilder, terminalBlock.getVariable(), mainJoinParts, context, terminalBlock); + } + } + + /** + * if(sb.length() > prefixLength) sb.append(","); + */ + private static class LengthBasedJoiningTerminal extends JoiningTerminal { + + protected LengthBasedJoiningTerminal(@NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull PrefixSuffixContext prefixSuffixContext, + @NotNull List delimiter, + @NotNull TerminalBlock block) { + super(block, targetBuilder, variable, mainJoinParts, prefixSuffixContext.getPrefixJoinParts(), + prefixSuffixContext.getSuffixJoinParts(), delimiter, prefixSuffixContext.getBeforeLoopStatement(), + prefixSuffixContext.getAfterLoopStatement()); + } + + @Nullable + static LengthBasedJoiningTerminal extractLengthBasedTerminal(@NotNull TerminalBlock terminalBlock, + @Nullable List nonFinalVariables) { + if (nonFinalVariables != null && !nonFinalVariables.isEmpty()) return null; + ArrayList statements = ContainerUtil.newArrayList(terminalBlock.getStatements()); + if (statements.size() < 2) return null; + PsiIfStatement ifStatement = tryCast(statements.get(0), PsiIfStatement.class); + if (ifStatement == null) return null; + PsiExpression condition = ifStatement.getCondition(); + if (condition == null || ifStatement.getElseBranch() != null) return null; + List delimiter = extractDelimiter(ifStatement); + if (delimiter == null) return null; + List withoutCondition = statements.subList(1, statements.size()); + PsiLocalVariable targetBuilder = extractStringBuilder(withoutCondition.get(0)); + if (targetBuilder == null) return null; + Integer conditionPrefixLength = extractConditionPrefixLength(condition, targetBuilder); + if (conditionPrefixLength == null) return null; + + List mainJoinParts = extractJoinParts(withoutCondition); + if (mainJoinParts == null) return null; + PsiLoopStatement loop = terminalBlock.getMainLoop(); + PrefixSuffixContext context = + PrefixSuffixContext.extractAndVerifyRefs(loop, loop, targetBuilder, terminalBlock, emptyList(), new HashSet<>(emptyList())); + if (context == null) return null; + String prefix = computeConstant(context.getPrefixJoinParts()); + if (prefix == null || prefix.length() != conditionPrefixLength) return null; + return new LengthBasedJoiningTerminal(targetBuilder, terminalBlock.getVariable(), mainJoinParts, context, delimiter, terminalBlock); + } + + @Nullable + private static List extractDelimiter(PsiIfStatement ifStatement) { + PsiStatement thenBranch = ifStatement.getThenBranch(); + if (thenBranch == null) return null; + List delimiterAppendStatements = Arrays.asList(ControlFlowUtils.unwrapBlock(thenBranch)); + List delimiterJoinParts = extractJoinParts(delimiterAppendStatements); + if (delimiterJoinParts == null) return null; + if(computeConstant(delimiterJoinParts) == null) return null; + return delimiterJoinParts; + } + } + + /** + * if(first) sb.append(mainPart) else sb.append(delimiter).append(","); + */ + private static class BoolFlagJoiningTerminal extends JoiningTerminal { + + private final @NotNull PsiVariable myBoolVariable; + + protected BoolFlagJoiningTerminal(@NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull PrefixSuffixContext prefixSuffixContext, + @NotNull List delimiter, + @NotNull PsiVariable boolVariable, + @NotNull TerminalBlock block) { + super(block, targetBuilder, variable, mainJoinParts, prefixSuffixContext.getPrefixJoinParts(), + prefixSuffixContext.getSuffixJoinParts(), delimiter, prefixSuffixContext.getBeforeLoopStatement(), + prefixSuffixContext.getAfterLoopStatement()); + this.myBoolVariable = boolVariable; + } + + @Override + void preCleanUp() { + super.preCleanUp(); + myBoolVariable.delete(); + } + + @Nullable + static JoiningTerminal extractBoolFlagTerminal(@NotNull TerminalBlock terminalBlock, + @Nullable List nonFinalVariables) { + if (nonFinalVariables != null && nonFinalVariables.size() != 1) return null; + SpecialFirstIterationLoop specialFirstIterationLoop = SpecialFirstIterationLoop.BoolFlagLoop.extract(terminalBlock); + if (specialFirstIterationLoop == null) return null; + PsiLocalVariable boolVar = specialFirstIterationLoop.getVariable(); + if (boolVar == null) return null; + if (nonFinalVariables != null && !nonFinalVariables.get(0).equals(boolVar)) return null; + List firstIterationStatements = specialFirstIterationLoop.getFirstIterationStatements(); + List otherIterationStatements = specialFirstIterationLoop.getOtherIterationStatements(); + if (firstIterationStatements.isEmpty() || otherIterationStatements.isEmpty()) return null; + + List firstIterationJoinParts = extractJoinParts(firstIterationStatements); + List otherIterationJoinParts = extractJoinParts(otherIterationStatements); + + if (firstIterationJoinParts == null || otherIterationJoinParts == null) return null; + JoinData joinData = JoinData.extractLeftDelimiter(otherIterationJoinParts); + if (!joinPartsAreEquivalent(joinData.getMainJoinParts(), firstIterationJoinParts)) return null; + + + PsiLocalVariable targetBuilder = extractStringBuilder(firstIterationStatements.get(0)); + if (targetBuilder == null) return null; + PsiLoopStatement loop = terminalBlock.getMainLoop(); + PrefixSuffixContext context = + PrefixSuffixContext.extractAndVerifyRefs(loop, loop, targetBuilder, terminalBlock, singletonList(boolVar), + new HashSet<>(emptyList())); + if (context == null) return null; + + return new BoolFlagJoiningTerminal(targetBuilder, terminalBlock.getVariable(), firstIterationJoinParts, context, + joinData.getDelimiterJoinParts(), boolVar, terminalBlock); + } + } + + /** + * for() ... + * if(sb.length() > prefixLength) sb.seLength(sb.length() - delimiterSize) + */ + private static class LengthTruncateJoiningTerminal extends JoiningTerminal { + private final @NotNull PsiIfStatement myTruncateIfStatement; + + protected LengthTruncateJoiningTerminal(@NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull PrefixSuffixContext prefixSuffixContext, + @NotNull List delimiter, + @NotNull PsiIfStatement truncateIfStatement, + @NotNull TerminalBlock block) { + super(block, targetBuilder, variable, mainJoinParts, prefixSuffixContext.getPrefixJoinParts(), + prefixSuffixContext.getSuffixJoinParts(), delimiter, prefixSuffixContext.getBeforeLoopStatement(), + prefixSuffixContext.getAfterLoopStatement()); + myTruncateIfStatement = truncateIfStatement; + } + + void preCleanUp() { + super.preCleanUp(); + myTruncateIfStatement.delete(); + } + + @Nullable + static LengthTruncateJoiningTerminal extractLengthTruncateTerminal(@NotNull TerminalBlock terminalBlock, + @Nullable List nonFinalVariables) { + if (nonFinalVariables != null && !nonFinalVariables.isEmpty()) return null; + List statements = Arrays.asList(terminalBlock.getStatements()); + if (statements.size() < 1) return null; + PsiLocalVariable targetBuilder = extractStringBuilder(statements.get(0)); + if (targetBuilder == null) return null; + List joinParts = extractJoinParts(statements); + if (joinParts == null) return null; + JoinData joinData = JoinData.extractRightDelimiter(joinParts); + List mainJoinParts = joinData.getMainJoinParts(); + List delimiterJoinParts = joinData.getDelimiterJoinParts(); + + PsiLoopStatement loop = terminalBlock.getMainLoop(); + + PsiIfStatement ifStatement = tryCast(PsiTreeUtil.skipWhitespacesAndCommentsForward(loop), PsiIfStatement.class); + if(ifStatement == null) return null; + PsiExpression condition = ifStatement.getCondition(); + if (condition == null) return null; + Integer conditionPrefixLength = extractConditionPrefixLength(condition, targetBuilder); + if (conditionPrefixLength == null) return null; + + + PsiMethodCallExpression truncateCall = extractTruncateCall(targetBuilder, ifStatement); + if (truncateCall == null) return null; + + Integer truncateSize = tryExtractTruncationSize(targetBuilder, truncateCall); + String delimiter = joinData.getDelimiter(); + if(delimiter == null) return null; + if (truncateSize == null || truncateSize != delimiter.length()) return null; + + PrefixSuffixContext context = + PrefixSuffixContext + .extractAndVerifyRefs(ifStatement, loop, targetBuilder, terminalBlock, emptyList(), new HashSet<>(singletonList(truncateCall))); + if (context == null) return null; + + + String prefix = computeConstant(context.getPrefixJoinParts()); + if (prefix == null || prefix.length() != conditionPrefixLength) return null; + PsiVariable loopVariable = terminalBlock.getVariable(); + return new LengthTruncateJoiningTerminal(targetBuilder, loopVariable, mainJoinParts, context, delimiterJoinParts, ifStatement, + terminalBlock); + } + + @Nullable + private static PsiMethodCallExpression extractTruncateCall(@NotNull PsiVariable targetBuilder, @NotNull PsiIfStatement ifStatement) { + if (ifStatement.getElseBranch() != null) return null; + PsiStatement block = ifStatement.getThenBranch(); + PsiStatement[] thenBranch = ControlFlowUtils.unwrapBlock(block); + if (thenBranch.length != 1) return null; + PsiExpressionStatement expressionStatement = tryCast(thenBranch[0], PsiExpressionStatement.class); + if(expressionStatement == null) return null; + PsiMethodCallExpression call = tryCast(expressionStatement.getExpression(), PsiMethodCallExpression.class); + if (!SET_LENGTH.test(call)) return null; + PsiLocalVariable localVariable = resolveLocalVariable(call.getMethodExpression().getQualifierExpression()); + + if (!targetBuilder.equals(localVariable)) return null; + return call; + } + + @Nullable + private static Integer tryExtractTruncationSize(@NotNull PsiVariable targetBuilder, + @NotNull PsiMethodCallExpression truncateCall) { + PsiExpression[] expressions = truncateCall.getArgumentList().getExpressions(); + if (expressions.length == 0) return null; + PsiExpression parameter = expressions[0]; + if (parameter == null) return null; + PsiBinaryExpression binaryExpression = tryCast(parameter, PsiBinaryExpression.class); + if (binaryExpression == null || !binaryExpression.getOperationTokenType().equals(JavaTokenType.MINUS)) return null; + PsiExpression lOperand = binaryExpression.getLOperand(); + PsiExpression rOperand = binaryExpression.getROperand(); + if (rOperand == null) return null; + Object constantExpression = ExpressionUtils.computeConstantExpression(rOperand); + if (!(constantExpression instanceof Integer)) return null; + int truncationSize = (int)constantExpression; + + PsiMethodCallExpression lengthCall = tryCast(lOperand, PsiMethodCallExpression.class); + if (!LENGTH.test(lengthCall)) return null; + PsiLocalVariable variable = resolveLocalVariable(lengthCall.getMethodExpression().getQualifierExpression()); + if (variable == null || !variable.equals(targetBuilder)) return null; + return truncationSize; + } + } + + /** + * String delimiter = ""; + * for() { + * sb.append(mainPart).append(delimiter); + * delimiter = ","; + * } + */ + private static class DelimiterRewriteJoiningTerminal extends JoiningTerminal { + private final @NotNull PsiVariable myDelimiterVariable; + + protected DelimiterRewriteJoiningTerminal(@NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull PrefixSuffixContext prefixSuffixContext, + @NotNull List delimiter, + @NotNull PsiVariable delimiterVariable, + @NotNull TerminalBlock block) { + super(block, targetBuilder, variable, mainJoinParts, prefixSuffixContext.getPrefixJoinParts(), + prefixSuffixContext.getSuffixJoinParts(), delimiter, prefixSuffixContext.getBeforeLoopStatement(), + prefixSuffixContext.getAfterLoopStatement()); + myDelimiterVariable = delimiterVariable; + } + + void preCleanUp() { + super.preCleanUp(); + myDelimiterVariable.delete(); + } + + @Nullable + static DelimiterRewriteJoiningTerminal extractDelimiterRewritingTerminal(@NotNull TerminalBlock terminalBlock, + @Nullable List nonFinalVariables) { + if (nonFinalVariables != null && nonFinalVariables.size() != 1) return null; + List statements = ContainerUtil.newArrayList(terminalBlock.getStatements()); + if (statements.size() < 2) return null; + // TODO maybe not just last, but check if delimiter not used after assignment? + PsiAssignmentExpression assignment = extractAssignment(statements.get(statements.size() - 1)); + if (assignment == null) return null; + PsiLocalVariable delimiterVar = extractDelimiterVar(assignment); + if (delimiterVar == null) return null; + PsiExpression delimiter = extractDelimiter(assignment); + if (delimiter == null) return null; + List mainStatements = statements.subList(0, statements.size() - 1); + List joinParts = extractJoinParts(mainStatements); + if (joinParts == null || joinParts.isEmpty()) return null; + + if (isSeparator(delimiterVar, joinParts.get(0))) return null; + joinParts.remove(0); + if (ReferencesSearch.search(delimiterVar, new LocalSearchScope(terminalBlock.getStatements())).findAll().size() != 2) return null; + + PsiLocalVariable targetBuilder = extractStringBuilder(mainStatements.get(0)); + if (targetBuilder == null) return null; + + PsiLoopStatement loop = terminalBlock.getMainLoop(); + PrefixSuffixContext context = + PrefixSuffixContext.extractAndVerifyRefs(loop, loop, targetBuilder, terminalBlock, singletonList(delimiterVar), + new HashSet<>(emptyList())); + if (context == null) return null; + PsiVariable variable = terminalBlock.getVariable(); + return new DelimiterRewriteJoiningTerminal(targetBuilder, variable, joinParts, context, singletonList(delimiter), delimiterVar, + terminalBlock); + } + + private static boolean isSeparator(PsiLocalVariable delimiterVar, PsiExpression joinPart) { + PsiLocalVariable maybeDelimiter = resolveLocalVariable(joinPart); + if(maybeDelimiter == null || !maybeDelimiter.equals(delimiterVar)) return true; + return false; + } + + @Nullable + private static PsiExpression extractDelimiter(@NotNull PsiAssignmentExpression assignmentExpression) { + PsiExpression expression = assignmentExpression.getRExpression(); + if (expression == null) return null; + Object constantExpression = ExpressionUtils.computeConstantExpression(expression); + if (!(constantExpression instanceof String)) return null; + return expression; + } + + @Nullable + private static PsiLocalVariable extractDelimiterVar(@NotNull PsiAssignmentExpression assignmentExpression) { + PsiLocalVariable delimiterVar = resolveLocalVariable(assignmentExpression.getLExpression()); + if (delimiterVar == null) return null; + PsiType delimiterVarType = delimiterVar.getType(); + if (!delimiterVarType.equalsToText(CommonClassNames.JAVA_LANG_STRING)) return null; + PsiExpression initializer = delimiterVar.getInitializer(); + if (initializer == null) return null; + Object constantExpression = ExpressionUtils.computeConstantExpression(initializer); + if (!"".equals(constantExpression)) return null; + return delimiterVar; + } + + @Nullable + private static PsiAssignmentExpression extractAssignment(@NotNull PsiStatement last) { + PsiExpressionStatement expressionStatement = tryCast(last, PsiExpressionStatement.class); + if (expressionStatement == null) return null; + PsiAssignmentExpression assignment = tryCast(expressionStatement.getExpression(), PsiAssignmentExpression.class); + if (assignment == null) return null; + return assignment; + } + } + + /** + * if(i > 0) append(","); + */ + private static class IndexBasedJoiningTerminal extends JoiningTerminal { + + protected IndexBasedJoiningTerminal(@NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull PrefixSuffixContext prefixSuffixContext, + @NotNull List delimiter, + @NotNull TerminalBlock block) { + super(block, targetBuilder, variable, mainJoinParts, prefixSuffixContext.getPrefixJoinParts(), + prefixSuffixContext.getSuffixJoinParts(), delimiter, prefixSuffixContext.getBeforeLoopStatement(), + prefixSuffixContext.getAfterLoopStatement()); + } + + @Nullable + static JoiningTerminal extractIndexBasedTerminal(@NotNull TerminalBlock terminalBlock, + @Nullable List nonFinalVariables) { + if (nonFinalVariables != null && !nonFinalVariables.isEmpty()) return null; + SpecialFirstIterationLoop specialFirstIterationLoop = SpecialFirstIterationLoop.IndexBasedLoop.extract(terminalBlock); + if (specialFirstIterationLoop == null) return null; + List firstIterationStatements = specialFirstIterationLoop.getFirstIterationStatements(); + List otherIterationStatements = specialFirstIterationLoop.getOtherIterationStatements(); + if (firstIterationStatements.isEmpty() || otherIterationStatements.isEmpty()) return null; + + List firstIterationJoinParts = extractJoinParts(firstIterationStatements); + List otherIterationJoinParts = extractJoinParts(otherIterationStatements); + + if (firstIterationJoinParts == null || otherIterationJoinParts == null) return null; + JoinData joinData = JoinData.extractLeftDelimiter(otherIterationJoinParts); + if (!joinPartsAreEquivalent(joinData.getMainJoinParts(), firstIterationJoinParts)) return null; + + + PsiLocalVariable targetBuilder = extractStringBuilder(firstIterationStatements.get(0)); + if (targetBuilder == null) return null; + PsiLoopStatement loop = terminalBlock.getMainLoop(); + PrefixSuffixContext context = + PrefixSuffixContext.extractAndVerifyRefs(loop, loop, targetBuilder, terminalBlock, emptyList(), new HashSet<>(emptyList())); + if (context == null) return null; + return new IndexBasedJoiningTerminal(targetBuilder, terminalBlock.getVariable(), firstIterationJoinParts, context, + joinData.getDelimiterJoinParts(), terminalBlock); + } + } + + + /** + * sb.append(elements[0]); + * for(int i = 1; i < elements.length; i++) { + * sb.append(delimiter).append(element[i]); + * } + */ + private static class CountedLoopJoiningTerminal extends JoiningTerminal { + + @NotNull private final StreamApiMigrationInspection.CountingLoopSource mySource; + @NotNull private final PsiStatement myBeforeLoopAppend; + + protected CountedLoopJoiningTerminal(@NotNull PsiLocalVariable targetBuilder, + @NotNull PsiVariable variable, + @NotNull List mainJoinParts, + @NotNull PrefixSuffixContext prefixSuffixContext, + @NotNull List delimiter, + @NotNull TerminalBlock block, + @NotNull StreamApiMigrationInspection.CountingLoopSource newSource, + @NotNull PsiStatement beforeLoopAppendStatement) { + super(block, targetBuilder, variable, mainJoinParts, prefixSuffixContext.getPrefixJoinParts(), + prefixSuffixContext.getSuffixJoinParts(), delimiter, prefixSuffixContext.getBeforeLoopStatement(), + prefixSuffixContext.getAfterLoopStatement()); + mySource = newSource; + myBeforeLoopAppend = beforeLoopAppendStatement; + } + + @Override + void preCleanUp() { + super.preCleanUp(); + myBeforeLoopAppend.delete(); + } + + private static List copyReplacingVar(@NotNull List joinParts, + @NotNull PsiLocalVariable localVariable, + @NotNull PsiExpression replacement) { + List copies = joinParts.stream().map(expression -> (PsiExpression)expression.copy()).collect(Collectors.toList()); + for (PsiElement joinPart : copies) { + ReferencesSearch.search(localVariable, new LocalSearchScope(joinPart)).forEach(reference -> { + reference.getElement().replace(replacement); + }); + } + return copies; + } + + @NotNull + @Override + String generateStreamCode() { + return mySource.createReplacement() + generateIntermediate() + generateTerminal(); + } + + @Nullable + static CountedLoopJoiningTerminal extractCountedLoopTerminal(@NotNull TerminalBlock terminalBlock, + @Nullable List nonFinalVariables) { + if (nonFinalVariables != null && !nonFinalVariables.isEmpty()) return null; + StreamApiMigrationInspection.CountingLoopSource loopSource = + terminalBlock.getLastOperation(StreamApiMigrationInspection.CountingLoopSource.class); + if (loopSource == null) return null; + PsiExpression initializer = loopSource.getVariable().getInitializer(); + Object constantExpression = ExpressionUtils.computeConstantExpression(initializer); + if (!Integer.valueOf(1).equals(constantExpression)) return null; + List statements = ContainerUtil.newArrayList(terminalBlock.getStatements()); + if (statements.isEmpty()) return null; + List joinParts = extractJoinParts(statements); + if (joinParts == null) return null; + JoinData joinData = JoinData.extractLeftDelimiter(joinParts); + List delimiterJoinParts = joinData.getDelimiterJoinParts(); + if (delimiterJoinParts.isEmpty()) return null; + PsiLoopStatement loop = terminalBlock.getMainLoop(); + PsiLocalVariable variable = tryCast(terminalBlock.getVariable(), PsiLocalVariable.class); + if (variable == null) return null; + PsiLocalVariable targetBuilder = extractStringBuilder(statements.get(0)); + if (targetBuilder == null) return null; + PsiMethodCallExpression beforeLoopAppend = JoiningTerminal.getCallBeforeStatement(loop, targetBuilder, APPEND, emptyList()); + if (beforeLoopAppend == null) return null; + PsiStatement beforeLoopAppendStatement = PsiTreeUtil.getParentOfType(beforeLoopAppend, PsiStatement.class); + if (beforeLoopAppendStatement == null) return null; + List firstIterationJoinParts = extractJoinParts(beforeLoopAppend); + if (firstIterationJoinParts == null) return null; + + + PsiElementFactory factory = JavaPsiFacade.getElementFactory(targetBuilder.getProject()); + PsiExpression expression = factory.createExpressionFromText("0", variable); + List replacedMainJoinParts = copyReplacingVar(joinData.getMainJoinParts(), variable, expression); + if (!joinPartsAreEquivalent(replacedMainJoinParts, firstIterationJoinParts)) return null; + PrefixSuffixContext context = + PrefixSuffixContext.extractAndVerifyRefs(loop, beforeLoopAppendStatement, targetBuilder, terminalBlock, emptyList(), + new HashSet<>(singletonList(beforeLoopAppend))); + if (context == null) return null; + + StreamApiMigrationInspection.CountingLoopSource newSource = loopSource.withInitializer(expression); + + return new CountedLoopJoiningTerminal(targetBuilder, variable, joinData.getMainJoinParts(), context, + delimiterJoinParts, terminalBlock, newSource, beforeLoopAppendStatement); + } + } + + private static class JoinData { // TODO confusing naming + private final @Nullable String myDelimiter; + private final @NotNull List myMainJoinParts; + private final @NotNull List myDelimiterJoinParts; + + public JoinData(@Nullable String delimiter, + @NotNull List mainJoinParts, + @NotNull List delimiterJoinParts) { + myDelimiter = delimiter; + myMainJoinParts = mainJoinParts; + myDelimiterJoinParts = delimiterJoinParts; + } + + @Nullable + public String getDelimiter() { + return myDelimiter; + } + + @NotNull + public List getMainJoinParts() { + return myMainJoinParts; + } + + @NotNull + public List getDelimiterJoinParts() { + return myDelimiterJoinParts; + } + + @NotNull + static JoinData extractLeftDelimiter(@NotNull List joinParts) { + List delimiterJoinParts = new ArrayList<>(); + int firstNonConstant = -1; + StringBuilder sb = new StringBuilder(); + for (int i = 0, size = joinParts.size(); i < size; i++) { + PsiExpression joinPart = joinParts.get(i); + String constantExpression = computeConstant(joinPart); + if (constantExpression == null) { + firstNonConstant = i; + break; + } + delimiterJoinParts.add(joinPart); + sb.append(constantExpression); + } + String separator = sb.length() == 0 ? null : sb.toString(); + if (firstNonConstant != -1) { + List mainJoinParts = joinParts.subList(firstNonConstant, joinParts.size()); + return new JoinData(separator, mainJoinParts, delimiterJoinParts); + } + return new JoinData(separator, emptyList(), delimiterJoinParts); + } + + @NotNull + static JoinData extractRightDelimiter(@NotNull List joinParts) { + List delimiterJoinParts = new ArrayList<>(); + int firstNonConstant = -1; + StringBuilder sb = new StringBuilder(); + for (int i = joinParts.size() - 1; i >= 0; i--) { + PsiExpression joinPart = joinParts.get(i); + String constantExpression = computeConstant(joinPart); + if (constantExpression == null) { + firstNonConstant = i; + break; + } + sb.append(constantExpression); + delimiterJoinParts.add(joinPart); + } + String separator = sb.length() == 0 ? null : sb.toString(); + if (firstNonConstant != -1) { + List mainJoinParts = joinParts.subList(0, firstNonConstant + 1); + return new JoinData(separator, mainJoinParts, delimiterJoinParts); + } + return new JoinData(separator, emptyList(), delimiterJoinParts); + } + } + } +} diff --git a/java/java-impl/src/com/intellij/codeInspection/streamMigration/SpecialFirstIterationLoop.java b/java/java-impl/src/com/intellij/codeInspection/streamMigration/SpecialFirstIterationLoop.java new file mode 100644 index 000000000000..3a6839a1b3a1 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/streamMigration/SpecialFirstIterationLoop.java @@ -0,0 +1,329 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection.streamMigration; + + +import com.intellij.psi.*; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.util.ThreeState; +import com.intellij.util.containers.ContainerUtil; +import com.siyeh.ig.psiutils.BoolUtils; +import com.siyeh.ig.psiutils.ExpressionUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import static com.intellij.util.ObjectUtils.tryCast; +import static com.siyeh.ig.psiutils.ExpressionUtils.resolveLocalVariable; + +class SpecialFirstIterationLoop { + private final @NotNull List myFirstIterationStatements; + private final @NotNull List myOtherIterationStatements; + private final @Nullable PsiLocalVariable myVariable; + + public SpecialFirstIterationLoop(@NotNull List firstIterationStatements, + @NotNull List otherIterationStatements, + @Nullable PsiLocalVariable variable) { + myFirstIterationStatements = firstIterationStatements; + myOtherIterationStatements = otherIterationStatements; + myVariable = variable; + } + + + @NotNull + public List getOtherIterationStatements() { + return myOtherIterationStatements; + } + + @NotNull + public List getFirstIterationStatements() { + return myFirstIterationStatements; + } + + @Nullable + public PsiLocalVariable getVariable() { + return myVariable; + } + + @Nullable + private static PsiExpression getExpressionComparedEqWithZero(@NotNull PsiBinaryExpression binaryExpression) { + if (!binaryExpression.getOperationTokenType().equals(JavaTokenType.EQEQ)) return null; + PsiExpression rOperand = binaryExpression.getROperand(); + if (rOperand == null) return null; + PsiExpression lOperand = binaryExpression.getLOperand(); + if (ExpressionUtils.isZero(lOperand)) return rOperand; + if (ExpressionUtils.isZero(rOperand)) return lOperand; + return null; + } + + @Contract("null -> null") + @Nullable + static PsiExpression getExpressionComparedToZero(@Nullable PsiBinaryExpression condition) { + if (condition == null) return null; + IElementType tokenType = condition.getOperationTokenType(); + PsiExpression left = condition.getLOperand(); + PsiExpression right = condition.getROperand(); + if (ExpressionUtils.isZero(right)) { + if (tokenType.equals(JavaTokenType.NE) || tokenType.equals(JavaTokenType.GT)) return left; + } + else if (ExpressionUtils.isZero(left)) { + if (tokenType.equals(JavaTokenType.NE) || tokenType.equals(JavaTokenType.LT)) return right; + } + return null; + } + + @Nullable + private static SpecialFirstIterationLoop extract(boolean firstIterationThen, + int index, + @NotNull List statements, + @NotNull PsiLocalVariable checkVar) { + PsiStatement statement = statements.get(index); + PsiIfStatement ifStatement = tryCast(statement, PsiIfStatement.class); + if (ifStatement == null) return null; + List thenStatements = unwrapBlock(ifStatement.getThenBranch()); + List elseStatements = unwrapBlock(ifStatement.getElseBranch()); + return extract(firstIterationThen, index, thenStatements, elseStatements, statements, checkVar); + } + + @Nullable + private static SpecialFirstIterationLoop extract(boolean firstIterationThen, + int index, + @NotNull List thenStatements, + @NotNull List elseStatements, + @NotNull List statements, + @NotNull PsiLocalVariable checkVar) { + PsiStatement statement = statements.get(index); + PsiIfStatement ifStatement = tryCast(statement, PsiIfStatement.class); + if (ifStatement == null) return null; + List beforeStatements = statements.subList(0, index); + List afterStatements = statements.subList(index + 1, statements.size()); + + + ArrayList firstIteration = new ArrayList<>(beforeStatements); + ArrayList otherIterations = new ArrayList<>(beforeStatements); + + firstIteration.addAll(firstIterationThen ? thenStatements : elseStatements); + firstIteration.addAll(afterStatements); + + otherIterations.addAll(firstIterationThen ? elseStatements : thenStatements); + otherIterations.addAll(afterStatements); + + return new SpecialFirstIterationLoop(firstIteration, otherIterations, checkVar); + } + + /** + * @return index of PsiStatement if it is the only statement, matches predicate or -1 otherwise + */ + private static int getSingleStatementIndex(@NotNull List statements, @NotNull Predicate predicate) { + int index = -1; + for (int i = 0; i < statements.size(); i++) { + PsiStatement statement = statements.get(i); + if (!predicate.test(statement)) continue; + if (index != -1) return -1; + index = i; + } + return index; + } + + private static int getSingleAssignmentIndex(@NotNull List statements) { + return getSingleStatementIndex(statements, statement -> ExpressionUtils.getAssignment(statement) != null); + } + + + static class BoolFlagLoop { + private BoolFlagLoop(){} + /* + Cases: + if(first) { ... } else { ... } + if(!first) ... + if(notFirst) ... + */ + @Nullable + static SpecialFirstIterationLoop extract(TerminalBlock terminalBlock) { + ArrayList statements = ContainerUtil.newArrayList(terminalBlock.getStatements()); + int index = getSingleStatementIndex(statements, PsiIfStatement.class::isInstance); + if (index == -1) return null; + PsiStatement statement = statements.get(index); + PsiIfStatement ifStatement = tryCast(statement, PsiIfStatement.class); + if (ifStatement == null) return null; + ThreeState firstIterationThen = isFirstIterationThen(statement); + if (firstIterationThen.equals(ThreeState.UNSURE)) return null; + final ConditionData conditionData = ConditionData.extract(ifStatement, firstIterationThen.toBoolean()); + if(conditionData == null) return null; + + PsiAssignmentExpression assignment = conditionData.getAssignment(); + PsiExpression expression = assignment.getLExpression(); + PsiLocalVariable boolFlag = resolveLocalVariable(expression); + if(boolFlag == null) return null; + PsiExpression rExpression = assignment.getRExpression(); + if (rExpression == null) return null; + if (!assignmentNegatesInitializer(boolFlag, rExpression)) return null; + PsiExpression condition = ifStatement.getCondition(); + boolean referencesAllowed = + ReferencesSearch.search(boolFlag).forEach(reference -> PsiTreeUtil.isAncestor(condition, reference.getElement(), false) || + PsiTreeUtil.isAncestor(assignment, reference.getElement(), false) || + PsiTreeUtil.isAncestor(boolFlag, reference.getElement(), false)); + if (!referencesAllowed) return null; + return SpecialFirstIterationLoop + .extract(firstIterationThen.toBoolean(), index, conditionData.getThenStatements(), conditionData.getElseStatements(), statements, + boolFlag); + } + + private static boolean assignmentNegatesInitializer(@NotNull PsiVariable boolFlag, @NotNull PsiExpression expression) { + Object constantExpression = ExpressionUtils.computeConstantExpression(expression); + if (!(constantExpression instanceof Boolean)) return false; + boolean assignmentValue = (boolean)constantExpression; + return ExpressionUtils.isLiteral(PsiUtil.skipParenthesizedExprDown(boolFlag.getInitializer()), !assignmentValue); + } + + @NotNull + private static ThreeState isFirstIterationThen(@NotNull PsiStatement statement) { + PsiIfStatement ifStatement = tryCast(statement, PsiIfStatement.class); + if (ifStatement == null) return ThreeState.UNSURE; + PsiExpression condition = ifStatement.getCondition(); + if (condition == null) return ThreeState.UNSURE; + + boolean negated = BoolUtils.isNegation(condition); + condition = PsiUtil.skipParenthesizedExprDown(condition); + PsiExpression expression = negated ? BoolUtils.getNegated(condition) : condition; + PsiLocalVariable boolFlagVar = resolveLocalVariable(expression); + if(boolFlagVar == null) return ThreeState.UNSURE; + return ThreeState.fromBoolean(ExpressionUtils.isLiteral(PsiUtil.skipParenthesizedExprDown(boolFlagVar.getInitializer()), !negated)); + } + + + private static class ConditionData { + private final @NotNull List myThenStatements; + private final @NotNull List myElseStatements; + private final @NotNull PsiAssignmentExpression myAssignment; + + private ConditionData(@NotNull List thenStatements, + @NotNull List elseStatements, + @NotNull PsiAssignmentExpression assignment) { + myThenStatements = thenStatements; + myElseStatements = elseStatements; + myAssignment = assignment; + } + + @NotNull + public PsiAssignmentExpression getAssignment() { + return myAssignment; + } + + @NotNull + public List getElseStatements() { + return myElseStatements; + } + + @NotNull + public List getThenStatements() { + return myThenStatements; + } + + + @Nullable + static ConditionData extract(@NotNull PsiIfStatement ifStatement, boolean firstIterationThen) { + PsiStatement block = firstIterationThen ? ifStatement.getThenBranch() : ifStatement.getElseBranch(); + ArrayList firstIterationStatements = new ArrayList<>(unwrapBlock(block)); + int index = getSingleAssignmentIndex(firstIterationStatements); + if (index == -1) return null; + PsiStatement assignment = firstIterationStatements.remove(index); + PsiExpressionStatement expressionStatement = tryCast(assignment, PsiExpressionStatement.class); + if(expressionStatement == null) return null; + PsiAssignmentExpression assignmentExpression = tryCast(expressionStatement.getExpression(), PsiAssignmentExpression.class); + if(assignmentExpression == null) return null; + PsiStatement otherBlock = firstIterationThen ? ifStatement.getElseBranch() : ifStatement.getThenBranch(); + List otherIterationStatements = unwrapBlock(otherBlock); + return firstIterationThen + ? new ConditionData(firstIterationStatements, otherIterationStatements, assignmentExpression) + : new ConditionData(otherIterationStatements, firstIterationStatements, assignmentExpression); + } + } + } + + static class IndexBasedLoop{ + private IndexBasedLoop(){} + + /* + if(i == 0) { + sb.append(mainPart); + } else { + sb.append(",").append(mainPart); + } + + if(i > 0) { + sb.append(","); + } + sb.append(mainPart) + + if(i != 0) { + sb.append(","); + } + sb.append(mainPart) + */ + @Nullable + static SpecialFirstIterationLoop extract(@NotNull TerminalBlock terminalBlock) { + StreamApiMigrationInspection.CountingLoopSource countingLoopSource = + terminalBlock.getLastOperation(StreamApiMigrationInspection.CountingLoopSource.class); + if (countingLoopSource == null) return null; + PsiVariable loopVar = countingLoopSource.getVariable(); + PsiLocalVariable loopLocalVar = tryCast(loopVar, PsiLocalVariable.class); + if (loopLocalVar == null) return null; + + ArrayList statements = ContainerUtil.newArrayList(terminalBlock.getStatements()); + int index = getSingleStatementIndex(statements, statement -> statement instanceof PsiIfStatement); + if (index == -1) return null; + ThreeState firstIterationThen = isFirstIterationThen(statements.get(index), loopVar); + if (firstIterationThen.equals(ThreeState.UNSURE)) return null; + return SpecialFirstIterationLoop.extract(firstIterationThen.toBoolean(), index, statements, loopLocalVar); + } + + @NotNull + private static ThreeState isFirstIterationThen(@NotNull PsiStatement statement, @NotNull PsiVariable loopVar) { + PsiIfStatement ifStatement = tryCast(statement, PsiIfStatement.class); + if (ifStatement == null) return ThreeState.UNSURE; + PsiExpression condition = ifStatement.getCondition(); + if (condition == null) return ThreeState.UNSURE; + PsiBinaryExpression binaryExpression = tryCast(condition, PsiBinaryExpression.class); + if (binaryExpression == null) return ThreeState.UNSURE; + + PsiExpression comparedEqWithZero = getExpressionComparedEqWithZero(binaryExpression); + if (comparedEqWithZero != null) { + if (!ExpressionUtils.isReferenceTo(comparedEqWithZero, loopVar)) return ThreeState.UNSURE; + return ThreeState.YES; + } + PsiExpression notEqWithZero = getExpressionComparedToZero(binaryExpression); + if (notEqWithZero == null || !ExpressionUtils.isReferenceTo(notEqWithZero, loopVar)) return ThreeState.UNSURE; + return ThreeState.NO; + } + } + + @NotNull + private static List unwrapBlock(@Nullable PsiStatement statement) { + if(statement == null) return Collections.emptyList(); + PsiBlockStatement blockStatement = tryCast(statement, PsiBlockStatement.class); + if(blockStatement == null) return Collections.singletonList(statement); + return ContainerUtil.newArrayList(blockStatement.getCodeBlock().getStatements()); + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/codeInspection/streamMigration/StreamApiMigrationInspection.java b/java/java-impl/src/com/intellij/codeInspection/streamMigration/StreamApiMigrationInspection.java index 7b1a2d4d2672..6714a9a1ed25 100644 --- a/java/java-impl/src/com/intellij/codeInspection/streamMigration/StreamApiMigrationInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/streamMigration/StreamApiMigrationInspection.java @@ -473,8 +473,8 @@ public class StreamApiMigrationInspection extends BaseJavaBatchLocalInspectionTo if (isCountOperation(nonFinalVariables, tb)) { return new CountMigration(true); } - if (nonFinalVariables.isEmpty()) { - CollectMigration.CollectTerminal terminal = CollectMigration.extractCollectTerminal(tb); + if (nonFinalVariables.size() == 0) { + CollectMigration.CollectTerminal terminal = CollectMigration.extractCollectTerminal(tb, nonFinalVariables); if (terminal != null) { boolean addAll = loop instanceof PsiForeachStatement && !tb.hasOperations() && isAddAllCall(tb); // Don't suggest to convert the loop which can be trivially replaced via addAll: @@ -487,6 +487,9 @@ public class StreamApiMigrationInspection extends BaseJavaBatchLocalInspectionTo return new CollectMigration(shouldWarn, terminal.getMethodName()); } } + if(JoiningMigration.extractTerminal(tb, nonFinalVariables) != null) { + return new JoiningMigration(true); + } if (tb.getCountExpression() != null || tb.isEmpty()) return null; if (nonFinalVariables.isEmpty() && extractArray(tb) != null) { return new ToArrayMigration(true); @@ -1102,6 +1105,10 @@ public class StreamApiMigrationInspection extends BaseJavaBatchLocalInspectionTo return new CountingLoopSource(getLoop(), getVariable(), getExpression(), bound, myIncluding); } + CountingLoopSource withInitializer(PsiExpression expression) { + return new CountingLoopSource(getLoop(), getVariable(), expression, myBound, myIncluding); + } + @Override boolean isWriteAllowed(PsiVariable variable, PsiExpression reference) { if (variable == myVariable) { diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoining.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoining.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoining.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoining.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningAppendNotJustBeforeLoop.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningAppendNotJustBeforeLoop.java new file mode 100644 index 000000000000..68196612c883 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningAppendNotJustBeforeLoop.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + private static String work2(List strs) { + String sb = strs.stream().collect(Collectors.joining(",", "{", "}")); + return sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningAssign.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningAssign.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningAssign.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningAssign.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharArray.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharArray.java new file mode 100644 index 000000000000..08dab8dcba3d --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharArray.java @@ -0,0 +1,13 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb; + System.out.println("hello"); + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> s.trim() + String.valueOf(new char[]{'a', 'c'}) + (5 + 6)).collect(Collectors.joining()); + return sb.length() == 0 ? null : sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharAt.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharAt.java new file mode 100644 index 000000000000..ed52eb7a6966 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningCharAt.java @@ -0,0 +1,13 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb; + System.out.println("hello"); + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> s.substring(0, 1)).collect(Collectors.joining()); + return sb.length() == 0 ? null : sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningComplexAppend.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningComplexAppend.java new file mode 100644 index 000000000000..9390942b8550 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningComplexAppend.java @@ -0,0 +1,13 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb; + System.out.println("hello"); + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> s.trim() + 12 + "asd" + s + 1).collect(Collectors.joining()); + return sb.length() == 0 ? null : sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningConcat.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningConcat.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningConcat.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningConcat.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningConstants.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningConstants.java new file mode 100644 index 000000000000..cf1af6b6c7aa --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningConstants.java @@ -0,0 +1,14 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Test { + static String test(List list) { + int BUFLENGTH = 42; + char CH = 'a'; + + String sb = IntStream.range(0, BUFLENGTH >> 1).mapToObj(i -> "\u041b" + CH + 'i').collect(Collectors.joining()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningDelimited.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimited.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningDelimited.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimited.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBool.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBool.java new file mode 100644 index 000000000000..6f85b7153063 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBool.java @@ -0,0 +1,19 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb = ""; + if (!list.isEmpty()) { + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> String.valueOf(s.length())).collect(Collectors.joining("\"")); + } + return sb.trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolCommon.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolCommon.java new file mode 100644 index 000000000000..0d95a5ce031f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolCommon.java @@ -0,0 +1,19 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb = ""; + if (!list.isEmpty()) { + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> s.length() + "!").collect(Collectors.joining("\"")); + } + return sb.trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolFlippedIf.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolFlippedIf.java new file mode 100644 index 000000000000..6f85b7153063 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolFlippedIf.java @@ -0,0 +1,19 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb = ""; + if (!list.isEmpty()) { + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> String.valueOf(s.length())).collect(Collectors.joining("\"")); + } + return sb.trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolNotFirst.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolNotFirst.java new file mode 100644 index 000000000000..6f85b7153063 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolNotFirst.java @@ -0,0 +1,19 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb = ""; + if (!list.isEmpty()) { + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> String.valueOf(s.length())).collect(Collectors.joining("\"")); + } + return sb.trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolPrefix.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolPrefix.java new file mode 100644 index 000000000000..fbdbeaf77aeb --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedBoolPrefix.java @@ -0,0 +1,19 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb = ""; + if (!list.isEmpty()) { + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> String.valueOf(s.length())).collect(Collectors.joining("\"", "{", "")); + } + return sb.trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedConstant.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedConstant.java new file mode 100644 index 000000000000..2785e498df77 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedConstant.java @@ -0,0 +1,20 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + char CONST_DELIMITER = '"'; + String sb = ""; + if (!list.isEmpty()) { + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> String.valueOf(s.length())).collect(Collectors.joining(String.valueOf(CONST_DELIMITER))); + } + return sb.trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedTruncated.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedTruncated.java new file mode 100644 index 000000000000..5abeae05ba20 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimitedTruncated.java @@ -0,0 +1,16 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb = list.stream().filter(s -> !s.isEmpty()).map(s -> String.valueOf(s.length())).collect(Collectors.joining("\"")); + return sb.trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimiterRewrite.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimiterRewrite.java new file mode 100644 index 000000000000..68196612c883 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningDelimiterRewrite.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + private static String work2(List strs) { + String sb = strs.stream().collect(Collectors.joining(",", "{", "}")); + return sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningFinalSb.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningFinalSb.java new file mode 100644 index 000000000000..3d97138d6190 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningFinalSb.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + final String sb = list.stream().filter(s -> !s.isEmpty()).map(String::trim).collect(Collectors.joining("", "ctor" + "first", "")); + return sb.length() == 0 ? null : sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningInBrackets.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningInBrackets.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningInBrackets.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningInBrackets.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningUsingIndex.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndex.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningUsingIndex.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndex.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexEqZero.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexEqZero.java new file mode 100644 index 000000000000..c92a1d22513a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexEqZero.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + public void test(List list) { + String sb = list.stream().limit(10).collect(Collectors.joining(",")); + System.out.println(sb); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecond.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecond.java new file mode 100644 index 000000000000..c92a1d22513a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecond.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + public void test(List list) { + String sb = list.stream().limit(10).collect(Collectors.joining(",")); + System.out.println(sb); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecondPrefix.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecondPrefix.java new file mode 100644 index 000000000000..aa85f4d4bf45 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexFromSecondPrefix.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + public void test(List list) { + String sb = list.stream().limit(10).collect(Collectors.joining(",", "prefix", "")); + System.out.println(sb); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEq.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEq.java new file mode 100644 index 000000000000..c92a1d22513a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEq.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + public void test(List list) { + String sb = list.stream().limit(10).collect(Collectors.joining(",")); + System.out.println(sb); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEqElse.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEqElse.java new file mode 100644 index 000000000000..c92a1d22513a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningIndexNotEqElse.java @@ -0,0 +1,11 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + public void test(List list) { + String sb = list.stream().limit(10).collect(Collectors.joining(",")); + System.out.println(sb); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningInitialValue.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningInitialValue.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningInitialValue.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningInitialValue.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMixed.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMixed.java new file mode 100644 index 000000000000..f8f4454b06e6 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMixed.java @@ -0,0 +1,13 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb; + System.out.println("hello"); + sb = list.stream().filter(s -> !s.isEmpty()).map(s -> s.trim() + "foo" + 3 + (5 + 6)).collect(Collectors.joining()); + return sb.length() == 0 ? null : sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMultipleAppend.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMultipleAppend.java new file mode 100644 index 000000000000..cb73ffd13df1 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningMultipleAppend.java @@ -0,0 +1,13 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; + +public class Test { + static String test(List list) { + String sb; + boolean first = true; + sb = list.stream().map(s -> ", " + s.trim()).collect(Collectors.joining()); + return sb.length() == 0 ? null : sb; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningReused.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningReused.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningReused.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningReused.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningStringBuilderReused.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningStringBuilderReused.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/afterJoiningStringBuilderReused.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningStringBuilderReused.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningTernary.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningTernary.java new file mode 100644 index 000000000000..f20ad3fa4cc2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/afterJoiningTernary.java @@ -0,0 +1,12 @@ +// "Replace with collect" "true" + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Test { + static String test(List list) { + int BUFLENGTH = 42; + String sb = IntStream.range(0, BUFLENGTH >> 1).mapToObj(i -> true ? "a" : "b").collect(Collectors.joining()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoining.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoining.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoining.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoining.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningAppendNotJustBeforeLoop.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningAppendNotJustBeforeLoop.java new file mode 100644 index 000000000000..0b2cdcca77c8 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningAppendNotJustBeforeLoop.java @@ -0,0 +1,18 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + private static String work2(List strs) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + String separator = ""; + for (String str : strs) { + sb.append(separator); + sb.append(str); + separator = ","; + } + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningAssign.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningAssign.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningAssign.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningAssign.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningBuilderInSuffix.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningBuilderInSuffix.java new file mode 100644 index 000000000000..3a76ef90c4ec --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningBuilderInSuffix.java @@ -0,0 +1,17 @@ +// "Replace with collect" "false" + +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + System.out.println("hello"); + for(String s : list) { + if(!s.isEmpty()) { + sb.append(s.trim()); + } + } + sb.append(sb.length()); + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharArray.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharArray.java new file mode 100644 index 000000000000..f37cc0ef6aae --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharArray.java @@ -0,0 +1,16 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + System.out.println("hello"); + for(String s : list) { + if(!s.isEmpty()) { + sb.append(s.trim()).append(new char[]{'a', 'c'}).append(5 + 6); + } + } + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharAt.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharAt.java new file mode 100644 index 000000000000..ff03b87f3f70 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningCharAt.java @@ -0,0 +1,16 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + System.out.println("hello"); + for(String s : list) { + if(!s.isEmpty()) { + sb.append(s.charAt(0)); + } + } + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningComplexAppend.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningComplexAppend.java new file mode 100644 index 000000000000..10a2ca078ce9 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningComplexAppend.java @@ -0,0 +1,16 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + System.out.println("hello"); + for(String s : list) { + if(!s.isEmpty()) { + sb.append(s.trim()).append(12 + "asd" + s).append(1); + } + } + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningConcat.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningConcat.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningConcat.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningConcat.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningConstants.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningConstants.java new file mode 100644 index 000000000000..ca18f120fbc4 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningConstants.java @@ -0,0 +1,17 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + int BUFLENGTH = 42; + char CH = 'a'; + + StringBuffer sb = new StringBuffer(BUFLENGTH); + for (int i = 0; i < BUFLENGTH >> 1; i++) { + sb.append('\u041b'); + sb.append(CH); + sb.append('i'); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningDelimited.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimited.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningDelimited.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimited.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBadTruncateSize.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBadTruncateSize.java new file mode 100644 index 000000000000..2a08970cb717 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBadTruncateSize.java @@ -0,0 +1,23 @@ +// "Replace with collect" "false" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + for (String s : list) { + if (!s.isEmpty()) { + sb.append(s.length()).append('"'); + } + } + if (sb.length() > 0) { + sb.setLength(sb.length() - 2); + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBool.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBool.java new file mode 100644 index 000000000000..c42ed4b54950 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBool.java @@ -0,0 +1,28 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (!list.isEmpty()) { + for (String s : list) { + if (!s.isEmpty()) { + if (first) { + sb.append(s.length()); + first = false; + } else { + sb.append('"').append(s.length()); + } + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolCommon.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolCommon.java new file mode 100644 index 000000000000..1f7f77cd470e --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolCommon.java @@ -0,0 +1,29 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (!list.isEmpty()) { + for (String s : list) { + if (!s.isEmpty()) { + if (first) { + sb.append(s.length()); + first = false; + } else { + sb.append('"').append(s.length()); + } + sb.append("!"); + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolDifferentJoinParts.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolDifferentJoinParts.java new file mode 100644 index 000000000000..8d0c99efee61 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolDifferentJoinParts.java @@ -0,0 +1,28 @@ +// "Replace with collect" "false" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (!list.isEmpty()) { + for (String s : list) { + if (!s.isEmpty()) { + if (first) { + sb.append(s); + first = false; + } else { + sb.append('"').append(s.length()); + } + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolFlippedIf.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolFlippedIf.java new file mode 100644 index 000000000000..55961e752bf2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolFlippedIf.java @@ -0,0 +1,29 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (!list.isEmpty()) { + for (String s : list) { + if (!s.isEmpty()) { + if (!first) { + sb.append('"').append(s.length()); + } + else { + sb.append(s.length()); + first = false; + } + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotChanged.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotChanged.java new file mode 100644 index 000000000000..1d384d6e7468 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotChanged.java @@ -0,0 +1,27 @@ +// "Replace with collect" "false" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (!list.isEmpty()) { + for (String s : list) { + if (!s.isEmpty()) { + if (first) { + sb.append(s.length()); + } else { + sb.append('"').append(s.length()); + } + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotFirst.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotFirst.java new file mode 100644 index 000000000000..2d4439bdae12 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolNotFirst.java @@ -0,0 +1,28 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean notFirst = false; + if (!list.isEmpty()) { + for (String s : list) { + if (!s.isEmpty()) { + if (!notFirst) { + sb.append(s.length()); + notFirst = true; + } else { + sb.append('"').append(s.length()); + } + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolPrefix.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolPrefix.java new file mode 100644 index 000000000000..90a78f2cf332 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedBoolPrefix.java @@ -0,0 +1,29 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (!list.isEmpty()) { + sb.append("{") + for (String s : list) { + if (!s.isEmpty()) { + if (first) { + sb.append(s.length()); + first = false; + } else { + sb.append('"').append(s.length()); + } + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedConstant.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedConstant.java new file mode 100644 index 000000000000..de8b4eb23a0c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedConstant.java @@ -0,0 +1,26 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + char CONST_DELIMITER = '"'; + StringBuilder sb = new StringBuilder(); + if (!list.isEmpty()) { + for (String s : list) { + if (!s.isEmpty()) { + if (sb.length() > 0) { + sb.append(CONST_DELIMITER); + } + sb.append(s.length()); + } + } + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedTruncated.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedTruncated.java new file mode 100644 index 000000000000..a66b132159d1 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedTruncated.java @@ -0,0 +1,23 @@ +// "Replace with collect" "true" + +import java.util.Arrays; +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + for (String s : list) { + if (!s.isEmpty()) { + sb.append(s.length()).append('"'); + } + } + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + return sb.toString().trim(); + } + + public static void main(String[] args) { + System.out.println(test(Arrays.asList("abc", "", "xyz", "argh"))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningDelimitedWrongBuilder.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedWrongBuilder.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningDelimitedWrongBuilder.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimitedWrongBuilder.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewrite.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewrite.java new file mode 100644 index 000000000000..afc4f2861078 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewrite.java @@ -0,0 +1,18 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + private static String work2(List strs) { + StringBuilder sb = new StringBuilder(); + String separator = ""; + sb.append("{"); + for (String str : strs) { + sb.append(separator); + sb.append(str); + separator = ","; + } + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewriteNonEmptyInit.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewriteNonEmptyInit.java new file mode 100644 index 000000000000..4d8b57b2ebeb --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningDelimiterRewriteNonEmptyInit.java @@ -0,0 +1,18 @@ +// "Replace with collect" "false" + +import java.util.List; + +public class Test { + private static String work2(List strs) { + StringBuilder sb = new StringBuilder(); + String separator = "!!!!"; + sb.append("{"); + for (String str : strs) { + sb.append(separator); + sb.append(str); + separator = ","; + } + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningFinalSb.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningFinalSb.java new file mode 100644 index 000000000000..bfd7c94a32fe --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningFinalSb.java @@ -0,0 +1,16 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + final StringBuilder sb = new StringBuilder("ctor"); + sb.append("first") + for(String s : list) { + if(!s.isEmpty()) { + sb.append(s.trim()); + } + } + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningInBrackets.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningInBrackets.java similarity index 87% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningInBrackets.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningInBrackets.java index 51ea51482424..dc72ac6b8be0 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningInBrackets.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningInBrackets.java @@ -7,7 +7,7 @@ public class Test { StringBuilder sb = new StringBuilder("["); for (String s : list) { if (!s.isEmpty()) { - if(sb.length() > 0) sb.append(','); + if(sb.length() > 1) sb.append(','); sb.append(s); } } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningUsingIndex.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndex.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningUsingIndex.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndex.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexEqZero.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexEqZero.java new file mode 100644 index 000000000000..17dcbbece44f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningIndexEqZero.java @@ -0,0 +1,17 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + public void test(List list) { + StringBuilder sb = new StringBuilder(); + for (int i=0; i list) { + StringBuilder sb = new StringBuilder(); + sb.append(list.get(0)); + for (int i=1; i list) { + StringBuilder sb = new StringBuilder(); + sb.append("prefix"); + sb.append(list.get(0)); + for (int i=1; i list) { + StringBuilder sb = new StringBuilder(); + for (int i=0; i list) { + StringBuilder sb = new StringBuilder(); + for (int i=0; i list) { + StringBuilder sb = new StringBuilder(); + System.out.println("hello"); + Runnable r = () -> { + for(String s : list) { + if(!s.isEmpty()) { + sb.append(s.trim()); + } + } + } + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningLambdaAfterLoop.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningLambdaAfterLoop.java new file mode 100644 index 000000000000..32ce39631bfa --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningLambdaAfterLoop.java @@ -0,0 +1,16 @@ +// "Replace with collect" "false" + +import java.util.List; + +public class Test { + public static void test(List list) { + StringBuilder sb = new StringBuilder(); + if(!list.isEmpty()) { + for (String s : list) { + sb.append(s); + } + } + Runnable r = () -> System.out.println(sb.toString()); + r.run(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMixed.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMixed.java new file mode 100644 index 000000000000..b636009d2cfc --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMixed.java @@ -0,0 +1,16 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + System.out.println("hello"); + for(String s : list) { + if(!s.isEmpty()) { + sb.append(s.trim()).append("foo" + 3).append(5 + 6); + } + } + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMultipleAppend.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMultipleAppend.java new file mode 100644 index 000000000000..6b5b0c1ddab8 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningMultipleAppend.java @@ -0,0 +1,14 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for(String s : list) { + sb.append(", ").append(s.trim()); + } + return sb.length() == 0 ? null : sb.toString(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningReused.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningReused.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningReused.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningReused.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningStringBuilderReused.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningStringBuilderReused.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/collect/beforeJoiningStringBuilderReused.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningStringBuilderReused.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningTernary.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningTernary.java new file mode 100644 index 000000000000..fd78d2526940 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/joining/beforeJoiningTernary.java @@ -0,0 +1,13 @@ +// "Replace with collect" "true" + +import java.util.List; + +public class Test { + static String test(List list) { + int BUFLENGTH = 42; + StringBuffer sb = new StringBuffer(BUFLENGTH); + for (int i = 0; i < BUFLENGTH >> 1; i++) { + sb.append(true? "a" : "b"); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/StreamApiMigrationInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/StreamApiMigrationInspectionTest.java index ac4c97fbf5d9..f95a47f2d2df 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/StreamApiMigrationInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/StreamApiMigrationInspectionTest.java @@ -157,6 +157,14 @@ public class StreamApiMigrationInspectionTest { } } + + public static class JoiningTest extends StreamApiMigrationInspectionBaseTest { + @Override + String getFolder() { + return "joining"; + } + } + public static class LimitTest extends StreamApiMigrationInspectionBaseTest { @Override String getFolder() { diff --git a/platform/util/src/com/intellij/util/ObjectUtils.java b/platform/util/src/com/intellij/util/ObjectUtils.java index f349a1b580d7..3d1e3936bf0e 100644 --- a/platform/util/src/com/intellij/util/ObjectUtils.java +++ b/platform/util/src/com/intellij/util/ObjectUtils.java @@ -80,6 +80,7 @@ public class ObjectUtils { } @Contract("null, _ -> null") + @Nullable public static T tryCast(@Nullable Object obj, @NotNull Class clazz) { if (clazz.isInstance(obj)) { return clazz.cast(obj); diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ControlFlowUtils.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ControlFlowUtils.java index 90568bb4ac0b..32dc6d6f2464 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ControlFlowUtils.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ControlFlowUtils.java @@ -397,6 +397,16 @@ public class ControlFlowUtils { } } + + @NotNull + public static PsiStatement[] unwrapBlock(@Nullable PsiStatement statement) { + PsiBlockStatement block = ObjectUtils.tryCast(statement, PsiBlockStatement.class); + if (block != null) { + return block.getCodeBlock().getStatements(); + } + return statement == null ? PsiStatement.EMPTY_ARRAY : new PsiStatement[]{statement}; + } + public static boolean statementCompletesWithStatement(@NotNull PsiStatement containingStatement, @NotNull PsiStatement statement) { PsiElement statementToCheck = statement; while (true) { diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ExpressionUtils.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ExpressionUtils.java index bcedf023739f..dd8516ac8956 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ExpressionUtils.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ExpressionUtils.java @@ -1004,6 +1004,14 @@ public class ExpressionUtils { return expression; } + @Contract(value = "null -> null") + @Nullable + public static PsiLocalVariable resolveLocalVariable(@Nullable PsiExpression expression) { + PsiReferenceExpression referenceExpression = ObjectUtils.tryCast(expression, PsiReferenceExpression.class); + if(referenceExpression == null) return null; + return ObjectUtils.tryCast(referenceExpression.resolve(), PsiLocalVariable.class); + } + public static boolean isOctalLiteral(PsiLiteralExpression literal) { final PsiType type = literal.getType(); if (!PsiType.INT.equals(type) && !PsiType.LONG.equals(type)) {