IDEA-243025 Action to collapse several statements into a loop

GitOrigin-RevId: 5064b5ac2c2ec4390d9d082b7fded96e0c731732
This commit is contained in:
Tagir Valeev
2020-06-10 13:37:15 +07:00
committed by intellij-monorepo-bot
parent bb49afba0d
commit 03db4748c0
21 changed files with 409 additions and 1 deletions

View File

@@ -1935,6 +1935,10 @@
<className>com.intellij.codeInsight.intention.impl.UnrollLoopAction</className>
<category>Java/Control Flow</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.CollapseIntoLoopAction</className>
<category>Java/Control Flow</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.MoveIntoIfBranchesAction</className>
<category>Java/Control Flow</category>

View File

@@ -0,0 +1,226 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.intention.impl;
import com.intellij.codeInsight.CodeInsightUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.SelectionModel;
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.codeStyle.VariableKind;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.IncorrectOperationException;
import com.siyeh.ig.psiutils.EquivalenceChecker;
import com.siyeh.ig.psiutils.VariableNameGenerator;
import one.util.streamex.MoreCollectors;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.intellij.util.ObjectUtils.tryCast;
public class CollapseIntoLoopAction implements IntentionAction {
@Override
public @IntentionName @NotNull String getText() {
return JavaBundle.message("intention.name.collapse.into.loop");
}
@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return getText();
}
private static List<PsiStatement> extractStatements(Editor editor, PsiFile file) {
if (!(file instanceof PsiJavaFile) || !PsiUtil.isLanguageLevel5OrHigher(file)) return Collections.emptyList();
SelectionModel model = editor.getSelectionModel();
int startOffset = model.getSelectionStart();
int endOffset = model.getSelectionEnd();
PsiElement[] elements = CodeInsightUtil.findStatementsInRange(file, startOffset, endOffset);
return StreamEx.of(elements)
.map(e -> tryCast(e, PsiStatement.class))
.collect(MoreCollectors.ifAllMatch(Objects::nonNull, Collectors.toList()))
.orElse(Collections.emptyList());
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
List<PsiStatement> statements = extractStatements(editor, file);
return LoopModel.from(statements) != null;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
List<PsiStatement> statements = extractStatements(editor, file);
LoopModel model = LoopModel.from(statements);
if (model == null) return;
model.generate();
}
@Override
public boolean startInWriteAction() {
return true;
}
private static class LoopModel {
final @NotNull List<PsiExpression> myLoopElements;
final @NotNull List<PsiExpression> myExpressionsToReplace;
final @NotNull List<PsiStatement> myStatements;
final int myStatementCount;
final @Nullable PsiType myType;
private LoopModel(@NotNull List<PsiExpression> elements,
@NotNull List<PsiExpression> expressionsToReplace,
@NotNull List<PsiStatement> statements,
int count,
@Nullable PsiType type) {
myLoopElements = elements;
myExpressionsToReplace = expressionsToReplace;
myStatements = statements;
myStatementCount = count;
myType = type;
}
void generate() {
PsiStatement context = myStatements.get(0);
PsiElementFactory factory = JavaPsiFacade.getElementFactory(context.getProject());
String loopDeclaration;
String varName;
if (myType == null) {
int size = myStatements.size() / myStatementCount;
varName = new VariableNameGenerator(context, VariableKind.PARAMETER).byType(PsiType.INT).generate(true);
loopDeclaration = "for(int " + varName + "=0;" + varName + "<" + size + ";" + varName + "++)";
}
else {
varName = new VariableNameGenerator(context, VariableKind.PARAMETER).byType(myType).generate(true);
loopDeclaration = tryCollapseIntoCountingLoop(varName);
if (loopDeclaration == null) {
String container;
if (myType instanceof PsiPrimitiveType) {
container = "new " + myType.getCanonicalText() + "[]{" + StringUtil.join(myLoopElements, PsiElement::getText, ",") + "}";
}
else {
container = CommonClassNames.JAVA_UTIL_ARRAYS + ".asList(" + StringUtil.join(myLoopElements, PsiElement::getText, ",") + ")";
}
loopDeclaration = "for(" + myType.getCanonicalText() + " " + varName + ":" + container + ")";
}
}
PsiLoopStatement loop = (PsiLoopStatement)factory.createStatementFromText(loopDeclaration + " {}", context);
PsiCodeBlock block = ((PsiBlockStatement)Objects.requireNonNull(loop.getBody())).getCodeBlock();
PsiJavaToken brace = Objects.requireNonNull(block.getRBrace());
PsiExpression ref = factory.createExpressionFromText(varName, context);
myExpressionsToReplace.forEach(expr -> expr.replace(ref));
block.addRangeBefore(myStatements.get(0), myStatements.get(myStatementCount - 1), brace);
PsiElement origBlock = context.getParent();
JavaCodeStyleManager.getInstance(block.getProject()).shortenClassReferences(origBlock.addBefore(loop, myStatements.get(0)));
origBlock.deleteChildRange(myStatements.get(0), myStatements.get(myStatements.size() - 1));
}
private String tryCollapseIntoCountingLoop(String varName) {
if (!PsiType.INT.equals(myType) && !PsiType.LONG.equals(myType)) return null;
Long start = null;
Long step = null;
Long last = null;
for (PsiExpression element : myLoopElements) {
if (!(element instanceof PsiLiteralExpression)) return null;
Object value = ((PsiLiteralExpression)element).getValue();
if (!(value instanceof Integer) && !(value instanceof Long)) return null;
long cur = ((Number)value).longValue();
if (start == null) {
start = cur;
}
else if (step == null) {
step = cur - start;
if (step == 0) return null;
}
else if (cur - last != step || (step > 0 && cur < last) || (step < 0 && cur > last)) {
return null;
}
last = cur;
}
if (start == null || step == null) return null;
String suffix = PsiType.LONG.equals(myType) ? "L" : "";
String initial = myType.getCanonicalText() + " " + varName + "=" + start + suffix;
String condition =
varName + (step == 1 && last != (PsiType.LONG.equals(myType) ? Long.MAX_VALUE : Integer.MAX_VALUE) ? "<" + (last + 1) :
step == -1 && last != (PsiType.LONG.equals(myType) ? Long.MIN_VALUE : Integer.MIN_VALUE) ? ">" + (last - 1) :
(step < 0 ? ">=" : "<=") + last) + suffix;
String increment = varName + (step == 1 ? "++" : step == -1 ? "--" : step > 0 ? "+=" + step + suffix : "-=" + (-step) + suffix);
return "for(" + initial + ";" + condition + ";" + increment + ")";
}
static @Nullable LoopModel from(List<PsiStatement> statements) {
int size = statements.size();
if (size <= 1 || size > 1000) return null;
if (!(statements.get(0).getParent() instanceof PsiCodeBlock)) return null;
for (int count = 1; count <= size / 2; count++) {
if (size % count != 0) continue;
LoopModel model = from(statements, count);
if (model != null) {
return model;
}
}
return null;
}
private static @Nullable LoopModel from(List<PsiStatement> statements, int count) {
EquivalenceChecker equivalence = EquivalenceChecker.getCanonicalPsiEquivalence();
int size = statements.size();
PsiType type = null;
List<PsiExpression> expressionsToReplace = new ArrayList<>();
List<PsiExpression> expressionsToIterate = new ArrayList<>();
boolean secondIteration = true;
for (int offset = count; offset < size; offset += count) {
PsiExpression firstIterationExpression = null;
PsiExpression curIterationExpression = null;
for (int index = 0; index < count; index++) {
PsiStatement first = statements.get(index);
PsiStatement cur = statements.get(index + offset);
EquivalenceChecker.Match match = equivalence.statementsMatch(first, cur);
if (match.isExactMismatch()) return null;
if (match.isExactMatch()) continue;
PsiElement leftDiff = match.getLeftDiff();
PsiElement rightDiff = match.getRightDiff();
if (!(leftDiff instanceof PsiExpression) || !(rightDiff instanceof PsiExpression)) return null;
curIterationExpression = (PsiExpression)rightDiff;
firstIterationExpression = (PsiExpression)leftDiff;
if (secondIteration) {
if (!expressionsToReplace.isEmpty() &&
!equivalence.expressionsAreEquivalent(expressionsToReplace.get(0), (PsiExpression)leftDiff)) {
return null;
}
expressionsToReplace.add((PsiExpression)leftDiff);
}
else {
if (!expressionsToReplace.contains(leftDiff)) return null;
}
}
if (secondIteration) {
if (firstIterationExpression != null) {
expressionsToIterate.add(firstIterationExpression);
PsiType expressionType = GenericsUtil.getVariableTypeByExpressionType(firstIterationExpression.getType());
if (expressionType == null) return null;
type = expressionType;
}
}
secondIteration = false;
if (curIterationExpression != null) {
expressionsToIterate.add(curIterationExpression);
}
}
return new LoopModel(expressionsToIterate, expressionsToReplace, statements, count, type);
}
}
}

View File

@@ -0,0 +1,3 @@
for(String s : Arrays.asList("one", "two", "three")) {
System.out.println(s);
}

View File

@@ -0,0 +1,3 @@
System.out.println("one");
System.out.println("two");
System.out.println("three");

View File

@@ -0,0 +1,5 @@
<html>
<body>
This intention collapses the selected statements into the loop when possible.
</body>
</html>

View File

@@ -0,0 +1,8 @@
// "Collapse into loop" "true"
class X {
void test() {
for (int i = 10; i > 6; i--) {
System.out.println("Item: "+ i);
}
}
}

View File

@@ -0,0 +1,8 @@
// "Collapse into loop" "true"
class X {
void test() {
for (long l = 10L; l >= 0L; l -= 2L) {
System.out.println(l);
}
}
}

View File

@@ -0,0 +1,8 @@
// "Collapse into loop" "true"
class X {
void test() {
for (int i = 0; i < 6; i++) {
System.out.println("Hello");
}
}
}

View File

@@ -0,0 +1,8 @@
// "Collapse into loop" "true"
class X {
void test() {
for (int i : new int[]{1, 2, 3, 5, 8}) {
System.out.println(i);
}
}
}

View File

@@ -0,0 +1,8 @@
// "Collapse into loop" "true"
class X {
void test() {
for (int i = 1; i <= 9; i += 2) {
System.out.println(i);
}
}
}

View File

@@ -0,0 +1,10 @@
import java.util.Arrays;
// "Collapse into loop" "true"
class X {
void test() {
for (String s : Arrays.asList("Hello", "World")) {
System.out.println(s);
}
}
}

View File

@@ -0,0 +1,10 @@
// "Collapse into loop" "true"
class X {
void test(int[] data) {
for (int i = 0; i < 6; i++) {
System.out.print("data["+ i +"]");
System.out.println("=");
System.out.println(data[i]);
}
}
}

View File

@@ -0,0 +1,9 @@
// "Collapse into loop" "true"
class X {
void test() {
<selection>System.out.println("Item: "+10);
System.out.println("Item: "+9);
System.out.println("Item: "+8);
System.out.println("Item: "+7);</selection>
}
}

View File

@@ -0,0 +1,11 @@
// "Collapse into loop" "true"
class X {
void test() {
<selection>System.out.println(10L);
System.out.println(8L);
System.out.println(6L);
System.out.println(4L);
System.out.println(2L);
System.out.println(0L);</selection>
}
}

View File

@@ -0,0 +1,11 @@
// "Collapse into loop" "true"
class X {
void test() {
<selection>System.out.println("Hello");
System.out.println("Hello");
System.out.println("Hello");
System.out.println("Hello");
System.out.println("Hello");
System.out.println("Hello");</selection>
}
}

View File

@@ -0,0 +1,10 @@
// "Collapse into loop" "true"
class X {
void test() {
<selection>System.out.println(1);
System.out.println(2);
System.out.println(3);
System.out.println(5);
System.out.println(8);</selection>
}
}

View File

@@ -0,0 +1,10 @@
// "Collapse into loop" "true"
class X {
void test() {
<selection>System.out.println(1);
System.out.println(3);
System.out.println(5);
System.out.println(7);
System.out.println(9);</selection>
}
}

View File

@@ -0,0 +1,7 @@
// "Collapse into loop" "true"
class X {
void test() {
<selection>System.out.println("Hello");
System.out.println("World");</selection>
}
}

View File

@@ -0,0 +1,23 @@
// "Collapse into loop" "true"
class X {
void test(int[] data) {
<selection>System.out.print("data["+0+"]");
System.out.println("=");
System.out.println(data[0]);
System.out.print("data["+1+"]");
System.out.println("=");
System.out.println(data[1]);
System.out.print("data["+2+"]");
System.out.println("=");
System.out.println(data[2]);
System.out.print("data["+3+"]");
System.out.println("=");
System.out.println(data[3]);
System.out.print("data["+4+"]");
System.out.println("=");
System.out.println(data[4]);
System.out.print("data["+5+"]");
System.out.println("=");
System.out.println(data[5]);</selection>
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.java.codeInsight.intention;
import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
public class CollapseIntoLoopActionTest extends LightIntentionActionTestCase {
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/collapseIntoLoop";
}
}

View File

@@ -1254,4 +1254,5 @@ slice.usage.message.assertion.violated=(assertion violated!)
slice.usage.message.in.file.stopped.here=(in {0} file - stopped here)
slice.usage.message.tracking.container.contents=(Tracking container ''{0}{1}'' contents)
slice.usage.message.location=in {0}
intention.name.move.into.if.branches=Move up into 'if' statement branches
intention.name.move.into.if.branches=Move up into 'if' statement branches
intention.name.collapse.into.loop=Collapse into loop