mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 04:51:24 +07:00
IDEA-243025 Action to collapse several statements into a loop
GitOrigin-RevId: 5064b5ac2c2ec4390d9d082b7fded96e0c731732
This commit is contained in:
committed by
intellij-monorepo-bot
parent
bb49afba0d
commit
03db4748c0
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
for(String s : Arrays.asList("one", "two", "three")) {
|
||||
System.out.println(s);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
System.out.println("one");
|
||||
System.out.println("two");
|
||||
System.out.println("three");
|
||||
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
This intention collapses the selected statements into the loop when possible.
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Collapse into loop" "true"
|
||||
class X {
|
||||
void test() {
|
||||
for (int i = 10; i > 6; i--) {
|
||||
System.out.println("Item: "+ i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Collapse into loop" "true"
|
||||
class X {
|
||||
void test() {
|
||||
for (long l = 10L; l >= 0L; l -= 2L) {
|
||||
System.out.println(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Collapse into loop" "true"
|
||||
class X {
|
||||
void test() {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
System.out.println("Hello");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Collapse into loop" "true"
|
||||
class X {
|
||||
void test() {
|
||||
for (int i = 1; i <= 9; i += 2) {
|
||||
System.out.println(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Collapse into loop" "true"
|
||||
class X {
|
||||
void test() {
|
||||
<selection>System.out.println("Hello");
|
||||
System.out.println("World");</selection>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user