diff options
| author | Fabrice Tiercelin | 2020-10-20 19:09:56 +0000 |
|---|---|---|
| committer | Fabrice Tiercelin | 2020-10-30 18:19:18 +0000 |
| commit | 13ac98691c016d0b1d1e4a7a776c8ace36f1b8a7 (patch) | |
| tree | c4c13ce0751167d4ddd4736de899dfcb3abecc8d | |
| parent | 25a33bb2089eb4ff0201fb1399418b66179ef895 (diff) | |
| download | eclipse.jdt.ui-13ac98691c016d0b1d1e4a7a776c8ace36f1b8a7.tar.gz eclipse.jdt.ui-13ac98691c016d0b1d1e4a7a776c8ace36f1b8a7.tar.xz eclipse.jdt.ui-13ac98691c016d0b1d1e4a7a776c8ace36f1b8a7.zip | |
Bug 568050 - [AutoRefactor immigration #36/141] [cleanup & saveaction]I20201030-1800
Break loop
Change-Id: I468c93096ae66230d97e6d96ef013e6395e42097
Signed-off-by: Fabrice Tiercelin <fabrice.tiercelin@yahoo.fr>
13 files changed, 1040 insertions, 2 deletions
diff --git a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java index 11e30679f6..55959ff2cb 100644 --- a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java +++ b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java @@ -121,6 +121,7 @@ public class MultiFixMessages extends NLS { public static String NullAnnotationsCleanUp_add_nonnullbydefault_annotation; public static String NullAnnotationsCleanUp_remove_redundant_nullness_annotation; + public static String BreakLoopCleanUp_description; public static String CodeStyleCleanUp_LazyLogical_description; public static String PrimitiveSerializationCleanUp_description; diff --git a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties index 1837874a87..0623f6c42b 100644 --- a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties +++ b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties @@ -103,6 +103,7 @@ NullAnnotationsCleanUp_add_nonnull_annotation=Add missing @NonNull annotation NullAnnotationsCleanUp_add_nonnullbydefault_annotation=Add missing @NonNullByDefault annotation NullAnnotationsCleanUp_remove_redundant_nullness_annotation=Remove redundant nullness annotation +BreakLoopCleanUp_description=Exit loop earlier CodeStyleCleanUp_LazyLogical_description= Use lazy logical operator (&& and ||) PrimitiveSerializationCleanUp_description=Primitive serialization diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java index 29aaa9e0df..347d8e9371 100644 --- a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; @@ -804,6 +805,24 @@ public class ASTNodes { } /** + * Return the identifiers of variables declared inside the given statement. + * + * @param node The node to visit + * @param includeInnerScopes True if blocks are visited too. + * + * @return The ids of the declared variables. + */ + public static Set<SimpleName> getLocalVariableIdentifiers(final ASTNode node, final boolean includeInnerScopes) { + if (node == null) { + return Collections.emptySet(); + } + + VarDeclarationIdentifierVisitor visitor= new VarDeclarationIdentifierVisitor(node, includeInnerScopes); + node.accept(visitor); + return visitor.getVariableNames(); + } + + /** * Return true if the node changes nothing and throws no exceptions. * * @param node The node to visit. diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/VarDeclarationIdentifierVisitor.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/VarDeclarationIdentifierVisitor.java new file mode 100644 index 0000000000..86747ab3cf --- /dev/null +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/VarDeclarationIdentifierVisitor.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2020 Fabrice TIERCELIN and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Fabrice TIERCELIN - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.internal.corext.dom; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; + +/** + * Visitor collecting all definitions of any variable. + */ +public class VarDeclarationIdentifierVisitor extends ASTVisitor { + private final Set<SimpleName> variableNames= new HashSet<>(); + private final ASTNode startNode; + private final boolean includeInnerScopes; + + /** + * The constructor. + * + * @param startNode the {@link ASTNode} which is the scope of the search + * @param includeInnerScopes True if the sub blocks should be analyzed + */ + public VarDeclarationIdentifierVisitor(final ASTNode startNode, final boolean includeInnerScopes) { + this.startNode= startNode; + this.includeInnerScopes= includeInnerScopes; + } + + /** + * Get the variable names. + * + * @return the variable names. + */ + public Set<SimpleName> getVariableNames() { + return variableNames; + } + + @Override + public boolean visit(final SingleVariableDeclaration node) { + variableNames.add(node.getName()); + return true; + } + + @Override + public boolean visit(final VariableDeclarationFragment node) { + variableNames.add(node.getName()); + return true; + } + + @Override + public boolean visit(final Block node) { + return startNode == node || includeInnerScopes; + } +} diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/VarOccurrenceVisitor.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/VarOccurrenceVisitor.java new file mode 100644 index 0000000000..b7fcde08f6 --- /dev/null +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/VarOccurrenceVisitor.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2020 Fabrice TIERCELIN and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Fabrice TIERCELIN - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.internal.corext.dom; + +import java.util.Set; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.SimpleName; + +/** + * The variable occurrence visitor. + */ +public class VarOccurrenceVisitor extends InterruptibleVisitor { + private final Set<SimpleName> localVarIds; + private boolean varUsed; + private ASTNode startNode; + private final boolean includeInnerScopes; + + /** + * Returns true if at least one variable is used. + * + * @return True if at least one variable is used + */ + public boolean isVarUsed() { + return varUsed; + } + + /** + * The constructor. + * + * @param localVarIds The ids of the variable to search + * @param includeInnerScopes True if the sub blocks should be analyzed + */ + public VarOccurrenceVisitor(final Set<SimpleName> localVarIds, final boolean includeInnerScopes) { + this.localVarIds= localVarIds; + this.includeInnerScopes= includeInnerScopes; + } + + @Override + public void traverseNodeInterruptibly(final ASTNode aStartNode) { + this.startNode= aStartNode; + super.traverseNodeInterruptibly(this.startNode); + } + + @Override + public boolean visit(final SimpleName aVariable) { + for (SimpleName localVarId : localVarIds) { + if (localVarId.getIdentifier() != null && localVarId.getIdentifier().equals(aVariable.getIdentifier())) { + varUsed= true; + return interruptVisit(); + } + } + + return true; + } + + @Override + public boolean visit(final Block node) { + return startNode == node || includeInnerScopes; + } +} diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java index 056cbd9187..3eb860d4fc 100644 --- a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java @@ -961,6 +961,18 @@ public class CleanUpConstants { public static final String VARIABLE_DECLARATION_USE_TYPE_ARGUMENTS_FOR_RAW_TYPE_REFERENCES= "cleanup.use_arguments_for_raw_type_references"; //$NON-NLS-1$ /** + * Add a break to avoid passive for loop iterations. + * <p> + * Possible values: {TRUE, FALSE} + * <p> + * + * @see CleanUpOptionsCore#TRUE + * @see CleanUpOptionsCore#FALSE + * @since 4.18 + */ + public static final String BREAK_LOOP= "cleanup.break_loop"; //$NON-NLS-1$ + + /** * Removes unused imports. <br> * <br> * Possible values: {TRUE, FALSE}<br> diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java index 4aa3d26e8e..fc37f826f1 100644 --- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java +++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java @@ -11365,6 +11365,456 @@ public class CleanUpTest extends CleanUpTestCase { } @Test + public void testBreakLoop() throws Exception { + IPackageFragment pack= fSourceFolder.createPackageFragment("test1", false, null); + String input= "" // + + "package test1;\n" // + + "\n" // + + "public class E {\n" // + + " private int[] innerArray = new int[10];\n" // + + "\n" // + + " public String addBreak(int number) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakInForeachLoop(int[] array) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithField() {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < this.innerArray.length; i++) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithoutBlock(int[] array) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " // Keep this comment\n" // + + " if (i == 42)\n" // + + " isFound = true;\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakAfterSeveralAssignments(String[] array, boolean isFound, int count) {\n" // + + " for (String text : array) {\n" // + + " if (text == null) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " count = 1;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " if (isFound) {\n" // + + " return \"We have found \" + count + \" result(s)\";\n" // + + " } else {\n" // + + " return \"The result has not been found\";\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " public String addBreakAfterComplexAssignment(int[] array) {\n" // + + " int hourNumber = 0;\n" // + + "\n" // + + " for (int dayNumber : array) {\n" // + + " if (dayNumber == 7) {\n" // + + " // Keep this comment\n" // + + " hourNumber = 7 * 24;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return \"Hour number: \" + hourNumber;\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithTemporaryVariable(int number) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " int temporaryInteger = i * 3;\n" // + + "\n" // + + " if (temporaryInteger == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public boolean[] addBreakWithFixedAssignment(int number, int index) {\n" // + + " boolean[] isFound = new boolean[number];\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound[index] = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound;\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithUpdatedIterator(int number) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i++ == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "}\n"; + ICompilationUnit cu= pack.createCompilationUnit("E.java", input, false, null); + + enable(CleanUpConstants.BREAK_LOOP); + + String output= "" // + + "package test1;\n" // + + "\n" // + + "public class E {\n" // + + " private int[] innerArray = new int[10];\n" // + + "\n" // + + " public String addBreak(int number) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakInForeachLoop(int[] array) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithField() {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < this.innerArray.length; i++) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithoutBlock(int[] array) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " // Keep this comment\n" // + + " if (i == 42) {\n" // + + " isFound = true;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public String addBreakAfterSeveralAssignments(String[] array, boolean isFound, int count) {\n" // + + " for (String text : array) {\n" // + + " if (text == null) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " count = 1;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " if (isFound) {\n" // + + " return \"We have found \" + count + \" result(s)\";\n" // + + " } else {\n" // + + " return \"The result has not been found\";\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " public String addBreakAfterComplexAssignment(int[] array) {\n" // + + " int hourNumber = 0;\n" // + + "\n" // + + " for (int dayNumber : array) {\n" // + + " if (dayNumber == 7) {\n" // + + " // Keep this comment\n" // + + " hourNumber = 7 * 24;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return \"Hour number: \" + hourNumber;\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithTemporaryVariable(int number) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " int temporaryInteger = i * 3;\n" // + + "\n" // + + " if (temporaryInteger == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "\n" // + + " public boolean[] addBreakWithFixedAssignment(int number, int index) {\n" // + + " boolean[] isFound = new boolean[number];\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " // Keep this comment\n" // + + " isFound[index] = true;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound;\n" // + + " }\n" // + + "\n" // + + " public String addBreakWithUpdatedIterator(int number) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i++ == 42) {\n" // + + " // Keep this comment\n" // + + " isFound = true;\n" // + + " break;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : \"The result has not been found\";\n" // + + " }\n" // + + "}\n"; + + assertNotEquals("The class must be changed", input, output); + assertGroupCategoryUsed(new ICompilationUnit[] { cu }, new HashSet<>(Arrays.asList(MultiFixMessages.BreakLoopCleanUp_description))); + assertRefactoringResultAsExpected(new ICompilationUnit[] { cu }, new String[] { output }); + } + + @Test + public void testDoNotBreakLoop() throws Exception { + IPackageFragment pack= fSourceFolder.createPackageFragment("test1", false, null); + String sample= "" // + + "package test1;\n" // + + "\n" // + + "public class E {\n" // + + " private int crazyInteger = 0;\n" // + + "\n" // + + " public String doNotBreakWithoutAssignment(int number) {\n" // + + " boolean isFound = false;\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " }\n" // + + " }\n" // + + " return isFound ? \"The result has been found\" : (\"The result has not been found\");\n" // + + " }\n" // + + "\n" // + + " public String doNotBreakWithExternalIterator(int number) {\n" // + + " boolean isFound = false;\n" // + + " int i;\n" // + + " for (i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + " return isFound ? \"The result has been found\" : (\"The result has not been found on \" + i + \" iteration(s)\");\n" // + + " }\n" // + + "\n" // + + " public String doNotBreakWithActiveConditions(int number) {\n" // + + " boolean isFound = false;\n" // + + " for (int i = 0; i < number--; i++) {\n" // + + " if (i == 42) {\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : (\"The result has not been found on \" + number + \" iteration(s)\");\n" // + + " }\n" // + + "\n" // + + " public boolean[] doNotBreakWithChangingAssignment(int number) {\n" // + + " boolean[] hasNumber42 = new boolean[number];\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " hasNumber42[i] = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return hasNumber42;\n" // + + " }\n" // + + "\n" // + + " public int[] doNotBreakForeachLoopWithChangingAssignment(int[] input, int[] output) {\n" // + + " for (int i : input) {\n" // + + " if (i == 42) {\n" // + + " output[i] = 123456;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return output;\n" // + + " }\n" // + + "\n" // + + " public boolean[] doNotBreakWithActiveAssignment(int number, int index) {\n" // + + " boolean[] isFound = new boolean[number];\n" // + + "\n" // + + " for (int i = 0; i < number; i++) {\n" // + + " if (i == 42) {\n" // + + " isFound[index++] = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound;\n" // + + " }\n" // + + "\n" // + + " public String doNotBreakWithActiveUpdater(int number) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i = 0; i < number; i++, number--) {\n" // + + " if (i == 42) {\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? \"The result has been found\" : (\"The result has not been found on \" + number + \" iteration(s)\");\n" // + + " }\n" // + + "\n" // + + " public String doNotBreakWithSeveralConditions(int[] array) {\n" // + + " int tenFactor = 0;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " if (i == 10) {\n" // + + " tenFactor = 1;\n" // + + " }\n" // + + " if (i == 100) {\n" // + + " tenFactor = 2;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return \"The result: \" + tenFactor;\n" // + + " }\n" // + + "\n" // + + " public int doNotBreakWithActiveCondition(int[] array, int modifiedInteger) {\n" // + + " boolean isFound = false;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " if (i == modifiedInteger++) {\n" // + + " isFound = true;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return isFound ? 0 : modifiedInteger;\n" // + + " }\n" // + + "\n" // + + " public int doNotBreakWithActiveAssignment(int[] array, int modifiedInteger) {\n" // + + " int result = 0;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " if (i == 42) {\n" // + + " result = modifiedInteger++;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return result;\n" // + + " }\n" // + + "\n" // + + " public int doNotBreakWithVariableAssignment(int[] array) {\n" // + + " int result = 0;\n" // + + "\n" // + + " new Thread() {\n" // + + " @Override\n" // + + " public void run() {\n" // + + " while (crazyInteger++ < 10000) {}\n" // + + " }\n" // + + " }.start();\n" // + + "\n" // + + " for (int i : array) {\n" // + + " if (i == 42) {\n" // + + " result = crazyInteger;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return result;\n" // + + " }\n" // + + "\n" // + + " public String doNotRefactorWithSpecialAssignment(int[] array) {\n" // + + " int tenFactor = 0;\n" // + + "\n" // + + " for (int i : array) {\n" // + + " if (i == 10) {\n" // + + " tenFactor += 1;\n" // + + " }\n" // + + " }\n" // + + "\n" // + + " return \"The result: \" + tenFactor;\n" // + + " }\n" // + + "\n" // + + " public void doNotBreakInfiniteLoop(int[] array) {\n" // + + " int tenFactor = 0;\n" // + + "\n" // + + " for (;;) {\n" // + + " if (crazyInteger == 10) {\n" // + + " tenFactor = 1;\n" // + + " }\n" // + + " }\n" // + + " }\n" // + + "}\n"; + ICompilationUnit cu= pack.createCompilationUnit("E.java", sample, false, null); + + enable(CleanUpConstants.BREAK_LOOP); + + assertRefactoringHasNoChange(new ICompilationUnit[] { cu }); + } + + @Test public void testRegExPrecompilationInLambda() throws Exception { IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); String sample= "" // diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java index e107d5d302..52739ab96c 100644 --- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java +++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java @@ -82,6 +82,7 @@ public class CleanUpConstantsOptions extends CleanUpConstants { options.setOption(PREFER_BOOLEAN_LITERAL, CleanUpOptions.FALSE); // Optimization + options.setOption(BREAK_LOOP, CleanUpOptions.FALSE); options.setOption(USE_LAZY_LOGICAL_OPERATOR, CleanUpOptions.FALSE); options.setOption(PRIMITIVE_SERIALIZATION, CleanUpOptions.FALSE); @@ -210,6 +211,7 @@ public class CleanUpConstantsOptions extends CleanUpConstants { options.setOption(PREFER_BOOLEAN_LITERAL, CleanUpOptions.FALSE); // Optimization + options.setOption(BREAK_LOOP, CleanUpOptions.FALSE); options.setOption(USE_LAZY_LOGICAL_OPERATOR, CleanUpOptions.FALSE); options.setOption(PRIMITIVE_SERIALIZATION, CleanUpOptions.FALSE); diff --git a/org.eclipse.jdt.ui/plugin.xml b/org.eclipse.jdt.ui/plugin.xml index 61bf1549e6..fda14039a3 100644 --- a/org.eclipse.jdt.ui/plugin.xml +++ b/org.eclipse.jdt.ui/plugin.xml @@ -7116,9 +7116,14 @@ runAfter="org.eclipse.jdt.ui.cleanup.pull_up_assignment"> </cleanUp> <cleanUp + class="org.eclipse.jdt.internal.ui.fix.BreakLoopCleanUp" + id="org.eclipse.jdt.ui.cleanup.break_loop" + runAfter="org.eclipse.jdt.ui.cleanup.number_suffix"> + </cleanUp> + <cleanUp class="org.eclipse.jdt.internal.ui.fix.LazyLogicalCleanUp" id="org.eclipse.jdt.ui.cleanup.lazy_logical" - runAfter="org.eclipse.jdt.ui.cleanup.number_suffix"> + runAfter="org.eclipse.jdt.ui.cleanup.break_loop"> </cleanUp> <cleanUp class="org.eclipse.jdt.internal.ui.fix.PrimitiveSerializationCleanUp" diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/BreakLoopCleanUp.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/BreakLoopCleanUp.java new file mode 100644 index 0000000000..2ed3242d1d --- /dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/BreakLoopCleanUp.java @@ -0,0 +1,398 @@ +/******************************************************************************* + * Copyright (c) 2020 Fabrice TIERCELIN and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Fabrice TIERCELIN - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.internal.ui.fix; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.runtime.CoreException; + +import org.eclipse.text.edits.TextEditGroup; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Assignment; +import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.ClassInstanceCreation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.EnhancedForStatement; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.ForStatement; +import org.eclipse.jdt.core.dom.IfStatement; +import org.eclipse.jdt.core.dom.InfixExpression; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.PostfixExpression; +import org.eclipse.jdt.core.dom.PrefixExpression; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.Statement; +import org.eclipse.jdt.core.dom.SuperMethodInvocation; +import org.eclipse.jdt.core.dom.ThrowStatement; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.VariableDeclarationStatement; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; + +import org.eclipse.jdt.internal.corext.dom.ASTNodes; +import org.eclipse.jdt.internal.corext.dom.InterruptibleVisitor; +import org.eclipse.jdt.internal.corext.dom.VarOccurrenceVisitor; +import org.eclipse.jdt.internal.corext.fix.CleanUpConstants; +import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix; +import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalModel; +import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite; + +import org.eclipse.jdt.ui.cleanup.CleanUpRequirements; +import org.eclipse.jdt.ui.cleanup.ICleanUpFix; +import org.eclipse.jdt.ui.text.java.IProblemLocation; + +/** + * A fix that adds a break to avoid passive for loop iterations. + * + * The conditions of triggering are: + * <ul> + * <li>The structure of the code (for loop, including an if, including assignments)</li> + * <li>Two cases of reject: + * <ul> + * <li>The inner assignments should not do other different assignments in the future (assign other values or assign into other variables),</li> + * <li>No side effects after the first assignments.</li> + * </ul> + * </li> + * </ul> + */ +public class BreakLoopCleanUp extends AbstractMultiFix { + public BreakLoopCleanUp() { + this(Collections.emptyMap()); + } + + public BreakLoopCleanUp(Map<String, String> options) { + super(options); + } + + @Override + public CleanUpRequirements getRequirements() { + boolean requireAST= isEnabled(CleanUpConstants.BREAK_LOOP); + return new CleanUpRequirements(requireAST, false, false, null); + } + + @Override + public String[] getStepDescriptions() { + if (isEnabled(CleanUpConstants.BREAK_LOOP)) { + return new String[] { MultiFixMessages.BreakLoopCleanUp_description }; + } + + return new String[0]; + } + + @Override + public String getPreview() { + StringBuilder bld= new StringBuilder(); + bld.append("boolean isFound = false;\n"); //$NON-NLS-1$ + bld.append("for (int i = 0; i < number; i++) {\n"); //$NON-NLS-1$ + bld.append(" if (i == 42) {\n"); //$NON-NLS-1$ + bld.append(" isFound = true;\n"); //$NON-NLS-1$ + + if (isEnabled(CleanUpConstants.BREAK_LOOP)) { + bld.append(" break;\n"); //$NON-NLS-1$ + } + + bld.append(" }\n"); //$NON-NLS-1$ + bld.append("}\n"); //$NON-NLS-1$ + + if (!isEnabled(CleanUpConstants.BREAK_LOOP)) { + bld.append("\n"); //$NON-NLS-1$ + } + + return bld.toString(); + } + + @Override + protected ICleanUpFix createFix(CompilationUnit unit) throws CoreException { + if (!isEnabled(CleanUpConstants.BREAK_LOOP)) { + return null; + } + + final List<CompilationUnitRewriteOperation> rewriteOperations= new ArrayList<>(); + + unit.accept(new ASTVisitor() { + final class SideEffectVisitor extends InterruptibleVisitor { + private final Set<SimpleName> localVariableNames; + private boolean hasSideEffect; + + private SideEffectVisitor(final Set<SimpleName> localVariableNames) { + this.localVariableNames= localVariableNames; + } + + private boolean hasSideEffect() { + return hasSideEffect; + } + + @Override + public boolean visit(final Assignment node) { + if (!ASTNodes.hasOperator(node, Assignment.Operator.ASSIGN)) { + hasSideEffect= true; + return interruptVisit(); + } + + return visitVar(node.getLeftHandSide()); + } + + private boolean visitVar(final Expression modifiedVar) { + if (!(modifiedVar instanceof SimpleName)) { + hasSideEffect= true; + return interruptVisit(); + } + + boolean isFound= false; + + for (SimpleName localVariableName : localVariableNames) { + if (ASTNodes.isSameVariable(localVariableName, modifiedVar)) { + isFound= true; + break; + } + } + + if (!isFound) { + hasSideEffect= true; + return interruptVisit(); + } + + return true; + } + + @Override + public boolean visit(final PrefixExpression node) { + if (ASTNodes.hasOperator(node, PrefixExpression.Operator.INCREMENT, PrefixExpression.Operator.DECREMENT)) { + return visitVar(node.getOperand()); + } + + return true; + } + + @Override + public boolean visit(final PostfixExpression node) { + return visitVar(node.getOperand()); + } + + @Override + public boolean visit(final InfixExpression node) { + if (ASTNodes.hasOperator(node, InfixExpression.Operator.PLUS) && ASTNodes.hasType(node, String.class.getCanonicalName()) + && (mayCallImplicitToString(node.getLeftOperand()) + || mayCallImplicitToString(node.getRightOperand()) + || mayCallImplicitToString(node.extendedOperands()))) { + hasSideEffect= true; + return interruptVisit(); + } + + return true; + } + + private boolean mayCallImplicitToString(final List<Expression> extendedOperands) { + if (extendedOperands != null) { + for (Expression expression : extendedOperands) { + if (mayCallImplicitToString(expression)) { + return true; + } + } + } + + return false; + } + + private boolean mayCallImplicitToString(final Expression expression) { + return !ASTNodes.hasType(expression, String.class.getCanonicalName(), boolean.class.getSimpleName(), short.class.getSimpleName(), int.class.getSimpleName(), long.class.getSimpleName(), float.class.getSimpleName(), double.class.getSimpleName(), + Short.class.getCanonicalName(), Boolean.class.getCanonicalName(), Integer.class.getCanonicalName(), Long.class.getCanonicalName(), Float.class.getCanonicalName(), + Double.class.getCanonicalName()) && !(expression instanceof PrefixExpression) && !(expression instanceof InfixExpression) + && !(expression instanceof PostfixExpression); + } + + @Override + public boolean visit(final SuperMethodInvocation node) { + hasSideEffect= true; + return interruptVisit(); + } + + @Override + public boolean visit(final MethodInvocation node) { + hasSideEffect= true; + return interruptVisit(); + } + + @Override + public boolean visit(final ClassInstanceCreation node) { + hasSideEffect= true; + return interruptVisit(); + } + + @Override + public boolean visit(final ThrowStatement node) { + hasSideEffect= true; + return interruptVisit(); + } + } + + @Override + public boolean visit(final ForStatement node) { + Set<SimpleName> vars= new HashSet<>(); + + for (Expression initializer : (List<Expression>) node.initializers()) { + vars.addAll(ASTNodes.getLocalVariableIdentifiers(initializer, true)); + } + + if (node.getExpression() == null + || hasSideEffect(node.getExpression(), vars) + || node.updaters().isEmpty()) { + return true; + } + + for (Expression updater : (List<Expression>) node.updaters()) { + if (hasSideEffect(updater, vars)) { + return true; + } + } + + return visitLoopBody(node.getBody(), vars); + } + + private boolean hasSideEffect(final ASTNode node, final Set<SimpleName> allowedVars) { + SideEffectVisitor variableUseVisitor= new SideEffectVisitor(allowedVars); + variableUseVisitor.traverseNodeInterruptibly(node); + return variableUseVisitor.hasSideEffect(); + } + + @Override + public boolean visit(final EnhancedForStatement node) { + if (ASTNodes.isArray(node.getExpression())) { + Set<SimpleName> vars= new HashSet<>(); + vars.add(node.getParameter().getName()); + return visitLoopBody(node.getBody(), vars); + } + + return true; + } + + private boolean visitLoopBody(final Statement body, final Set<SimpleName> allowedVars) { + List<Statement> statements= ASTNodes.asList(body); + + if (statements == null || statements.isEmpty()) { + return true; + } + + for (int i= 0; i < statements.size() - 1; i++) { + Statement statement= statements.get(i); + allowedVars.addAll(ASTNodes.getLocalVariableIdentifiers(statement, true)); + + if (hasSideEffect(statement, allowedVars)) { + return true; + } + } + + IfStatement ifStatement= ASTNodes.as(statements.get(statements.size() - 1), IfStatement.class); + + if (ifStatement != null && ifStatement.getElseStatement() == null && !hasSideEffect(ifStatement.getExpression(), allowedVars)) { + List<Statement> assignments= ASTNodes.asList(ifStatement.getThenStatement()); + + if (areAssignmentsValid(allowedVars, assignments)) { + rewriteOperations.add(new BreakLoopOperation(ifStatement)); + return false; + } + } + + return true; + } + + private boolean areAssignmentsValid(final Set<SimpleName> allowedVars, final List<Statement> assignments) { + if (assignments.isEmpty()) { + return false; + } + + for (Statement statement : assignments) { + VariableDeclarationStatement variableDeclaration= ASTNodes.as(statement, VariableDeclarationStatement.class); + Assignment assignment= ASTNodes.asExpression(statement, Assignment.class); + + if (variableDeclaration != null) { + for (Object obj : variableDeclaration.fragments()) { + VariableDeclarationFragment fragment= (VariableDeclarationFragment) obj; + + if (!ASTNodes.isHardCoded(fragment.getInitializer())) { + return false; + } + } + } else if (assignment != null + && ASTNodes.hasOperator(assignment, Assignment.Operator.ASSIGN) + && ASTNodes.isHardCoded(assignment.getRightHandSide()) + && ASTNodes.isPassive(assignment.getLeftHandSide())) { + VarOccurrenceVisitor varOccurrenceVisitor= new VarOccurrenceVisitor(allowedVars, true); + varOccurrenceVisitor.traverseNodeInterruptibly(assignment.getLeftHandSide()); + + if (varOccurrenceVisitor.isVarUsed()) { + return false; + } + } else { + return false; + } + } + + return true; + } + }); + + if (rewriteOperations.isEmpty()) { + return null; + } + + return new CompilationUnitRewriteOperationsFix(MultiFixMessages.BreakLoopCleanUp_description, unit, + rewriteOperations.toArray(new CompilationUnitRewriteOperation[0])); + } + + @Override + public boolean canFix(final ICompilationUnit compilationUnit, final IProblemLocation problem) { + return false; + } + + @Override + protected ICleanUpFix createFix(final CompilationUnit unit, final IProblemLocation[] problems) throws CoreException { + return null; + } + + private static class BreakLoopOperation extends CompilationUnitRewriteOperation { + private final IfStatement ifStatement; + + public BreakLoopOperation(final IfStatement ifStatement) { + this.ifStatement= ifStatement; + } + + @Override + public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException { + ASTRewrite rewrite= cuRewrite.getASTRewrite(); + AST ast= cuRewrite.getRoot().getAST(); + TextEditGroup group= createTextEditGroup(MultiFixMessages.BreakLoopCleanUp_description, cuRewrite); + + if (ifStatement.getThenStatement() instanceof Block) { + ListRewrite listRewrite= rewrite.getListRewrite(ifStatement.getThenStatement(), Block.STATEMENTS_PROPERTY); + listRewrite.insertLast(ast.newBreakStatement(), group); + } else { + Block newBlock= ast.newBlock(); + newBlock.statements().add(ASTNodes.createMoveTarget(rewrite, ifStatement.getThenStatement())); + newBlock.statements().add(ast.newBreakStatement()); + ASTNodes.replaceButKeepComment(rewrite, ifStatement.getThenStatement(), newBlock, group); + } + } + } +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java index 88cf1e299b..7736259c69 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java @@ -88,8 +88,9 @@ public class CleanUpMessages extends NLS { public static String OptimizationTabPage_GroupName_Optimization; - public static String OptimizationTabPage_CheckboxName_PrimitiveSerialization; + public static String OptimizationTabPage_CheckboxName_BreakLoop; public static String OptimizationTabPage_CheckboxName_UseLazyLogicalOperator; + public static String OptimizationTabPage_CheckboxName_PrimitiveSerialization; public static String OptimizationTabPage_CheckboxName_PrecompileRegEx; public static String OptimizationTabPage_CheckboxName_NoStringCreation; public static String OptimizationTabPage_CheckboxName_BooleanLiteral; diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties index db8b091185..16c6aa0df9 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties @@ -66,6 +66,7 @@ CodeStyleTabPage_CheckboxName_SimplifyLambdaExpressionAndMethodRefSyntax=Simplif CodeStyleTabPage_RadioName_UseAnonymous=Use anonymous class OptimizationTabPage_GroupName_Optimization=Optimization +OptimizationTabPage_CheckboxName_BreakLoop=Exit &loop earlier OptimizationTabPage_CheckboxName_UseLazyLogicalOperator=Use la&zy logical operator OptimizationTabPage_CheckboxName_PrimitiveSerialization=&Primitive serialization OptimizationTabPage_CheckboxName_PrecompileRegEx=Precompile reused regular e&xpressions diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/OptimizationTabPage.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/OptimizationTabPage.java index 01de826586..e28c67be57 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/OptimizationTabPage.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/OptimizationTabPage.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.internal.corext.fix.CleanUpConstants; import org.eclipse.jdt.internal.ui.fix.AbstractCleanUp; import org.eclipse.jdt.internal.ui.fix.BooleanLiteralCleanUp; +import org.eclipse.jdt.internal.ui.fix.BreakLoopCleanUp; import org.eclipse.jdt.internal.ui.fix.LazyLogicalCleanUp; import org.eclipse.jdt.internal.ui.fix.NoStringCreationCleanUp; import org.eclipse.jdt.internal.ui.fix.PatternCleanUp; @@ -33,6 +34,7 @@ public final class OptimizationTabPage extends AbstractCleanUpTabPage { @Override protected AbstractCleanUp[] createPreviewCleanUps(Map<String, String> values) { return new AbstractCleanUp[] { + new BreakLoopCleanUp(values), new LazyLogicalCleanUp(values), new PrimitiveSerializationCleanUp(values), new PatternCleanUp(values), @@ -45,6 +47,9 @@ public final class OptimizationTabPage extends AbstractCleanUpTabPage { protected void doCreatePreferences(Composite composite, int numColumns) { Group optimizationGroup= createGroup(numColumns, composite, CleanUpMessages.OptimizationTabPage_GroupName_Optimization); + final CheckboxPreference breakLoopPref= createCheckboxPref(optimizationGroup, numColumns, CleanUpMessages.OptimizationTabPage_CheckboxName_BreakLoop, CleanUpConstants.BREAK_LOOP, CleanUpModifyDialog.FALSE_TRUE); + registerPreference(breakLoopPref); + final CheckboxPreference useLazyLogicalPref= createCheckboxPref(optimizationGroup, numColumns, CleanUpMessages.OptimizationTabPage_CheckboxName_UseLazyLogicalOperator, CleanUpConstants.USE_LAZY_LOGICAL_OPERATOR, CleanUpModifyDialog.FALSE_TRUE); registerPreference(useLazyLogicalPref); |
