diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/UnrollLoopAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/UnrollLoopAction.java new file mode 100644 index 000000000000..65dce6e74b89 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/UnrollLoopAction.java @@ -0,0 +1,158 @@ +/* + * 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.codeInsight.intention.impl; + +import com.intellij.codeInsight.CodeInsightBundle; +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; +import com.intellij.openapi.editor.Editor; +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.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.refactoring.util.InlineUtil; +import com.intellij.util.IncorrectOperationException; +import com.siyeh.ig.callMatcher.CallMatcher; +import com.siyeh.ig.psiutils.*; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Objects; + +public class UnrollLoopAction extends PsiElementBaseIntentionAction { + private static final CallMatcher LIST_CONSTRUCTOR = CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_ARRAYS, "asList"); + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull final PsiElement element) { + PsiForeachStatement loop = PsiTreeUtil.getParentOfType(element, PsiForeachStatement.class); + if (loop == null) return false; + if (!(loop.getParent() instanceof PsiCodeBlock)) return false; + PsiExpression iteratedValue = ExpressionUtils.resolveExpression(loop.getIteratedValue()); + if (extractExpressions(iteratedValue).length == 0) return false; + PsiStatement[] statements = ControlFlowUtils.unwrapBlock(loop.getBody()); + if (statements.length == 0) return false; + if (Arrays.stream(statements).anyMatch(PsiDeclarationStatement.class::isInstance)) return false; + if (isBreakChain(loop)) { + statements = Arrays.copyOfRange(statements, 0, statements.length - 1); + } + for (PsiStatement statement : statements) { + boolean acceptable = PsiTreeUtil.processElements(statement, e -> { + if (e instanceof PsiBreakStatement && ((PsiBreakStatement)e).findExitedStatement() == loop) return false; + if (e instanceof PsiContinueStatement && ((PsiContinueStatement)e).findContinuedStatement() == loop) return false; + return true; + }); + if (!acceptable) return false; + } + return true; + } + + @NotNull + private static PsiExpression[] extractExpressions(PsiExpression expression) { + expression = PsiUtil.skipParenthesizedExprDown(expression); + if (expression instanceof PsiArrayInitializerExpression) { + return ((PsiArrayInitializerExpression)expression).getInitializers(); + } + if (expression instanceof PsiNewExpression) { + PsiArrayInitializerExpression initializer = ((PsiNewExpression)expression).getArrayInitializer(); + return initializer == null ? PsiExpression.EMPTY_ARRAY : initializer.getInitializers(); + } + if (expression instanceof PsiMethodCallExpression) { + PsiMethodCallExpression call = (PsiMethodCallExpression)expression; + if (LIST_CONSTRUCTOR.test(call) && MethodCallUtils.isVarArgCall(call)) { + return call.getArgumentList().getExpressions(); + } + } + return PsiExpression.EMPTY_ARRAY; + } + + @NotNull + @Override + public String getText() { + return getFamilyName(); + } + + @Override + @NotNull + public String getFamilyName() { + return CodeInsightBundle.message("intention.unroll.loop.family"); + } + + @Override + public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException { + PsiForeachStatement loop = PsiTreeUtil.getParentOfType(element, PsiForeachStatement.class); + if (loop == null) return; + if (!(loop.getParent() instanceof PsiCodeBlock)) return; + boolean breakChain = isBreakChain(loop); + PsiExpression iteratedValue = loop.getIteratedValue(); + PsiExpression[] expressions = extractExpressions(ExpressionUtils.resolveExpression(iteratedValue)); + if (expressions.length == 0) return; + PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); + CommentTracker ct = new CommentTracker(); + PsiElement anchor = loop; + for (PsiExpression expression : expressions) { + PsiForeachStatement copy = (PsiForeachStatement)factory.createStatementFromText(ct.text(loop), element); + PsiParameter parameter = copy.getIterationParameter(); + for (PsiReference reference : ReferencesSearch.search(parameter, new LocalSearchScope(copy))) { + final PsiElement referenceElement = reference.getElement(); + if (referenceElement instanceof PsiJavaCodeReferenceElement) { + ct.markUnchanged(expression); + InlineUtil.inlineVariable(parameter, expression, (PsiJavaCodeReferenceElement)referenceElement); + } + } + PsiStatement body = copy.getBody(); + assert body != null; + if (body instanceof PsiBlockStatement) { + PsiElement[] children = ((PsiBlockStatement)body).getCodeBlock().getChildren(); + PsiElement parent = anchor.getParent(); + PsiElement currentAnchor = anchor; + // Skip {braces} + Arrays.stream(children, 1, children.length - 1).forEach(child -> parent.addBefore(child, currentAnchor)); + } + if (breakChain) { + PsiStatement lastStatement = PsiTreeUtil.getPrevSiblingOfType(anchor, PsiStatement.class); + if (lastStatement instanceof PsiIfStatement) { + PsiIfStatement ifStatement = (PsiIfStatement)lastStatement; + PsiExpression condition = Objects.requireNonNull(ifStatement.getCondition()); + PsiStatement thenBranch = Objects.requireNonNull(ifStatement.getThenBranch()); + String negated = BoolUtils.getNegatedExpressionText(condition); + condition.replace(factory.createExpressionFromText(negated, condition)); + PsiBlockStatement block = (PsiBlockStatement)thenBranch.replace(factory.createStatementFromText("{}", lastStatement)); + anchor = block.getCodeBlock().getLastChild(); + } + } + } + PsiLocalVariable variable = ExpressionUtils.resolveLocalVariable(iteratedValue); + if (variable != null) ct.delete(variable); + ct.deleteAndRestoreComments(loop); + } + + /** + * @param loop loop to test + * @return true if the last statement is "if(...) break" + */ + private static boolean isBreakChain(PsiForeachStatement loop) { + PsiStatement lastStatement = loop.getBody(); + if (lastStatement instanceof PsiBlockStatement) { + lastStatement = ControlFlowUtils.getLastStatementInBlock(((PsiBlockStatement)lastStatement).getCodeBlock()); + } + if (!(lastStatement instanceof PsiIfStatement)) return false; + PsiIfStatement ifStatement = (PsiIfStatement)lastStatement; + return ifStatement.getElseBranch() == null && + ifStatement.getCondition() != null && + ControlFlowUtils.statementBreaksLoop(ControlFlowUtils.stripBraces(ifStatement.getThenBranch()), loop); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollBreakLast.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollBreakLast.java new file mode 100644 index 000000000000..d9795e4ef4d9 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollBreakLast.java @@ -0,0 +1,17 @@ +// "Unroll loop" "true" +class Test { + void test(Object y) { + System.out.println((Object) "one"); + if (!(Math.random() > 0.5)) { + System.out.println((Object) 1); + if (!(Math.random() > 0.5)) { + System.out.println((Object) 1.0); + if (!(Math.random() > 0.5)) { + System.out.println((Object) 1.0f); + if (!(Math.random() > 0.5)) { + } + } + } + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollList.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollList.java new file mode 100644 index 000000000000..1b6f3ebc77fb --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollList.java @@ -0,0 +1,19 @@ +// "Unroll loop" "true" +import java.util.*; + +class Test { + void test() { + if (!"foo".isEmpty()) { + System.out.println("foo"); + } + if (!"bar".isEmpty()) { + System.out.println("bar"); + } + if (!"baz".isEmpty()) { + System.out.println("baz"); + } + if (!"".isEmpty()) { + System.out.println(""); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollSimple.java new file mode 100644 index 000000000000..562812f6c8b6 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollSimple.java @@ -0,0 +1,9 @@ +// "Unroll loop" "true" +class Test { + void test() { + System.out.println(1); + System.out.println(2); + System.out.println(3); + System.out.println(4); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollVar.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollVar.java new file mode 100644 index 000000000000..0966ce069248 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/afterUnrollVar.java @@ -0,0 +1,11 @@ +// "Unroll loop" "true" +class Test { + void test() { + foo(true); + unresolved(!true); + foo(false); + unresolved(!false); + } + + void foo(boolean b) {} +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollBreak.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollBreak.java new file mode 100644 index 000000000000..f078e3c89202 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollBreak.java @@ -0,0 +1,11 @@ +// "Unroll loop" "false" +class Test { + void test() { + for(Object x : new Object[] {"one", 1, 1.0, 1.0f}) { + if(Math.random() > 0.5) break; + System.out.println(x); + } + } + + void foo(boolean b) {} +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollBreakLast.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollBreakLast.java new file mode 100644 index 000000000000..3419a63df686 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollBreakLast.java @@ -0,0 +1,9 @@ +// "Unroll loop" "true" +class Test { + void test(Object y) { + for(Object x : new Object[] {"one", 1, 1.0, 1.0f}) { + System.out.println(x); + if(Math.random() > 0.5) break; + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollList.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollList.java new file mode 100644 index 000000000000..361be0f8e217 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollList.java @@ -0,0 +1,12 @@ +// "Unroll loop" "true" +import java.util.*; + +class Test { + void test() { + for(String s : Arrays.asList("foo", "bar", "baz", "")) { + if(!s.isEmpty()) { + System.out.println(s); + } + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollSimple.java new file mode 100644 index 000000000000..747c9203a561 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollSimple.java @@ -0,0 +1,8 @@ +// "Unroll loop" "true" +class Test { + void test() { + for(int i : new int[] {1,2,3,4}) { + System.out.println(i); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollVar.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollVar.java new file mode 100644 index 000000000000..c5324f16b96f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop/beforeUnrollVar.java @@ -0,0 +1,12 @@ +// "Unroll loop" "true" +class Test { + void test() { + boolean steps = {true, false}; + for(boolean step : steps) { + foo(step); + unresolved(!step); + } + } + + void foo(boolean b) {} +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/UnrollLoopActionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/UnrollLoopActionTest.java new file mode 100644 index 000000000000..d69cc6541089 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/UnrollLoopActionTest.java @@ -0,0 +1,28 @@ +/* + * 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 UnrollLoopActionTest extends LightIntentionActionTestCase { + + public void test() { doAllTests(); } + + @Override + protected String getBasePath() { + return "/codeInsight/daemonCodeAnalyzer/quickFix/unrollLoop"; + } +} diff --git a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties index c183189d70a6..9658e17f4466 100644 --- a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties +++ b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties @@ -555,4 +555,6 @@ intention.extract.set.from.comparison.chain.duplicates={0} has detected {1} code block.comment.intersects.existing.comment=Selected region intersects existing comment block.comment.wrapping.suffix=Selected region contains block comment suffix -block.comment.nested.comment=Selected region contained block {0, choice, 1#comment|2#comments},\nsurrounding ranges were commented. \ No newline at end of file +block.comment.nested.comment=Selected region contained block {0, choice, 1#comment|2#comments},\nsurrounding ranges were commented. + +intention.unroll.loop.family=Unroll loop \ No newline at end of file diff --git a/resources-en/src/intentionDescriptions/UnrollLoopAction/after.java.template b/resources-en/src/intentionDescriptions/UnrollLoopAction/after.java.template new file mode 100644 index 000000000000..26a02251ddb4 --- /dev/null +++ b/resources-en/src/intentionDescriptions/UnrollLoopAction/after.java.template @@ -0,0 +1,4 @@ +System.out.println(1); +System.out.println(2); +System.out.println(3); +System.out.println(4); \ No newline at end of file diff --git a/resources-en/src/intentionDescriptions/UnrollLoopAction/before.java.template b/resources-en/src/intentionDescriptions/UnrollLoopAction/before.java.template new file mode 100644 index 000000000000..9ab16daa110a --- /dev/null +++ b/resources-en/src/intentionDescriptions/UnrollLoopAction/before.java.template @@ -0,0 +1,3 @@ +for(int x : new int[] {1,2,3,4}) { + System.out.println(x); +} \ No newline at end of file diff --git a/resources-en/src/intentionDescriptions/UnrollLoopAction/description.html b/resources-en/src/intentionDescriptions/UnrollLoopAction/description.html new file mode 100644 index 000000000000..02a48ebbda30 --- /dev/null +++ b/resources-en/src/intentionDescriptions/UnrollLoopAction/description.html @@ -0,0 +1,5 @@ + + +

This intention unrolls loop over explicit list of values

+ + \ No newline at end of file diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index ad7df3968e60..8cfac0e4a9d0 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -1032,6 +1032,10 @@ com.intellij.codeInsight.intention.impl.ExtractIfConditionAction Java/Control Flow + + com.intellij.codeInsight.intention.impl.UnrollLoopAction + Java/Control Flow + com.intellij.codeInsight.intention.impl.AddNotNullAnnotationIntention Java/Annotations