[java, inspection] new inspection "while can be do-while" IDEA-250650

GitOrigin-RevId: bd2bd0053500b98a316330b08481018825e9211f
This commit is contained in:
Aleksey Dobrynin
2023-11-13 17:41:19 +00:00
committed by intellij-monorepo-bot
parent 4c9dabf404
commit e5b2194515
15 changed files with 642 additions and 201 deletions
@@ -2522,3 +2522,6 @@ inspection.redundant.embedded.expression.message.literal=Redundant embedded lite
inspection.redundant.embedded.expression.message.empty=Redundant empty embedded expression
inspection.redundant.embedded.expression.fix.family.name=Inline embedded expression
auto.closeable.resource.quickfix.preview=Add method <code>{0}</code> to the list of ignored methods
inspection.while.can.be.replaced.with.do.while.display.name='while' can be replaced with 'do while'
inspection.while.can.be.replaced.with.do.while.message=Replace 'while' with 'do while'
inspection.while.can.be.replaced.with.do.while.family.name=Replace 'while' with 'do while'
@@ -11,8 +11,6 @@ replace.for.each.loop.with.indexed.for.loop.intention.family.name=Replace for-ea
replace.for.each.loop.with.optimized.indexed.for.loop.intention.name=Replace for-each loop with optimized indexed 'for' loop
replace.for.each.loop.with.optimized.indexed.for.loop.intention.family.name=Replace for-each loop with optimized indexed for loop
replace.for.each.loop.with.iterator.for.loop.intention.family.name=Replace for-each loop with iterator for loop
replace.while.loop.with.do.while.loop.intention.name=Replace 'while' loop with 'do while' loop
replace.while.loop.with.do.while.loop.intention.family.name=Replace while loop with do while loop
replace.do.while.loop.with.while.loop.intention.name=Replace 'do while' loop with 'while' loop
replace.do.while.loop.with.while.loop.intention.family.name=Replace do while loop with while loop
#expand.boolean.intention.name=Convert to 'if else'
@@ -776,6 +776,10 @@
key="if.can.be.assertion.name" groupBundle="messages.InspectionsBundle"
groupKey="group.names.control.flow.issues" enabledByDefault="true" level="INFORMATION"
implementationClass="com.siyeh.ig.asserttoif.IfCanBeAssertionInspection"/>
<localInspection groupPath="Java" language="JAVA" shortName="WhileCanBeDoWhile" enabledByDefault="true" level="WEAK WARNING"
bundle="messages.InspectionGadgetsBundle" key="inspection.while.can.be.replaced.with.do.while.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.control.flow.issues"
implementationClass="com.siyeh.ig.whileloop.WhileCanBeDoWhileInspection"/>
<!--group.names.data.flow.issues-->
<localInspection groupPath="Java" language="JAVA" shortName="BooleanVariableAlwaysNegated" bundle="messages.InspectionGadgetsBundle"
@@ -254,12 +254,6 @@
<bundleName>messages.JavaBundle</bundleName>
<categoryKey>intention.category.control.flow</categoryKey>
</intentionAction>
<intentionAction>
<language>JAVA</language>
<className>com.siyeh.ipp.whileloop.ReplaceWhileLoopWithDoWhileLoopIntention</className>
<bundleName>messages.JavaBundle</bundleName>
<categoryKey>intention.category.control.flow</categoryKey>
</intentionAction>
<intentionAction>
<language>JAVA</language>
<className>com.siyeh.ipp.whileloop.ReplaceDoWhileLoopWithWhileLoopIntention</className>
@@ -0,0 +1,274 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.whileloop;
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.PsiReplacementUtil;
import com.siyeh.ig.psiutils.BoolUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.EquivalenceChecker;
import com.siyeh.ig.psiutils.TrackingEquivalenceChecker;
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 static com.intellij.psi.JavaTokenType.WHILE_KEYWORD;
public class WhileCanBeDoWhileInspection extends AbstractBaseJavaLocalInspectionTool {
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitWhileStatement(@NotNull PsiWhileStatement statement) {
final PsiStatement body = statement.getBody();
final PsiExpression condition = statement.getCondition();
if (condition == null) return;
PsiElement highlightElement = statement.getFirstChild();
while (highlightElement != null && highlightElement.getNode().getElementType() != WHILE_KEYWORD) {
highlightElement = highlightElement.getNextSibling();
}
highlightElement = highlightElement != null ? highlightElement : condition;
final DiffRange duplicateElements;
if (body != null) {
final Block bodyBlock = Block.init(body);
duplicateElements = bodyBlock.getDiffRange(Block.find(statement, bodyBlock.statements.size()));
}
else {
duplicateElements = null;
}
if (duplicateElements != null) {
holder.registerProblem(highlightElement,
InspectionGadgetsBundle.message("inspection.while.can.be.replaced.with.do.while.message"),
duplicateElements.type(), new ReplaceWhileWithDoWhileFix());
}
else {
holder.registerProblem(highlightElement,
InspectionGadgetsBundle.message("inspection.while.can.be.replaced.with.do.while.message"),
ProblemHighlightType.INFORMATION, new ReplaceWhileWithDoWhileFix());
}
}
};
}
private static class ReplaceWhileWithDoWhileFix extends PsiUpdateModCommandQuickFix {
@Override
public @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message(
"inspection.while.can.be.replaced.with.do.while.family.name");
}
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
final PsiWhileStatement statement = (PsiWhileStatement)element.getParent();
if (statement == null) return;
final PsiStatement body = statement.getBody();
final PsiExpression condition = statement.getCondition();
final boolean infiniteLoop = BoolUtils.isTrue(condition);
final DiffRange duplicateElements;
if (body != null) {
final Block bodyBlock = Block.init(body);
duplicateElements = bodyBlock.getDiffRange(Block.find(statement, bodyBlock.statements.size()));
}
else {
duplicateElements = null;
}
final StringBuilder result = new StringBuilder();
final CommentTracker tracker = new CommentTracker();
if (!infiniteLoop && duplicateElements == null) {
result.append("if(");
if (condition != null) {
result.append(tracker.text(condition));
}
result.append(") {\n");
}
if (body instanceof PsiBlockStatement blockStatement) {
result.append("do {");
final PsiCodeBlock codeBlock = blockStatement.getCodeBlock();
final PsiElement[] children = codeBlock.getChildren();
if (children.length > 2) {
for (int i = 1; i < children.length - 1; i++) {
final PsiElement child = children[i];
result.append(tracker.text(child));
}
}
result.append('}');
}
else if (body != null) {
result.append("do ").append(tracker.text(body)).append('\n');
}
result.append("while(");
if (condition != null) {
result.append(tracker.text(condition));
}
result.append(");");
if (!infiniteLoop && duplicateElements == null) {
result.append("\n}");
}
if (duplicateElements != null) {
statement.getParent().deleteChildRange(duplicateElements.first(), duplicateElements.last());
}
PsiReplacementUtil.replaceStatement(statement, result.toString(), tracker);
}
}
private record DiffRange(@NotNull PsiElement first, @NotNull PsiElement last, @NotNull ProblemHighlightType type) {
}
private static class Block {
private @NotNull final List<PsiStatement> statements;
private @NotNull final List<PsiComment> comments;
private final @Nullable PsiStatement block;
Block(@NotNull PsiBlockStatement block) {
this.block = block;
comments = new ArrayList<>();
statements = new ArrayList<>();
for (PsiElement element : block.getCodeBlock().getChildren()) {
if (element instanceof PsiComment comment) {
comments.add(comment);
}
else if (element instanceof PsiStatement statement) {
statements.add(statement);
}
}
}
Block(@NotNull List<PsiStatement> statements, @NotNull List<PsiComment> comments) {
this.statements = statements;
this.comments = comments;
this.block = null;
}
static Block init(@NotNull PsiStatement body) {
if (body instanceof PsiBlockStatement block) {
return new Block(block);
}
else {
return new Block(Collections.singletonList(body), Collections.emptyList());
}
}
static Block find(@NotNull PsiElement start, int maxSize) {
final List<PsiStatement> statements = new ArrayList<>(maxSize);
final List<PsiComment> comments = new ArrayList<>();
while (statements.size() < maxSize && (start = start.getPrevSibling()) != null) {
if (start instanceof PsiStatement statement) {
if (statements.isEmpty() && statement instanceof PsiBlockStatement block) {
return new Block(block);
}
else {
statements.add(statement);
}
}
else if (!statements.isEmpty() && start instanceof PsiComment comment) { // ignore comments between while and duplicate code
comments.add(comment);
}
}
Collections.reverse(statements);
Collections.reverse(comments);
return new Block(statements, comments);
}
@Nullable
DiffRange getDiffRange(@NotNull Block block) {
final EquivalenceChecker checker = new BreakTrackingEquivalenceChecker();
if (block.block != null && this.block != null) {
if (checker.statementsAreEquivalent(block.block, this.block)) {
return new DiffRange(block.block, block.block, equalsComments(comments, block.comments)
? ProblemHighlightType.WEAK_WARNING
: ProblemHighlightType.INFORMATION);
}
else {
return null;
}
}
else {
if (statements.size() != block.statements.size()) return null;
for (int i = 0; i < statements.size(); i++) {
if (!checker.statementsAreEquivalent(statements.get(i), block.statements.get(i))) {
return null;
}
}
return new DiffRange(block.statements.get(0), block.statements.get(block.statements.size() - 1),
equalsComments(comments, block.comments)
? ProblemHighlightType.WEAK_WARNING
: ProblemHighlightType.INFORMATION);
}
}
private static boolean equalsComments(@NotNull List<PsiComment> comments1, @NotNull List<PsiComment> comments2) {
if (comments1.size() != comments2.size()) return false;
for (int i = 0; i < comments1.size(); i++) {
if (!Objects.equals(comments1.get(i).getText().trim(),
comments2.get(i).getText().trim())) {
return false;
}
}
return true;
}
/**
* Expanding the possibilities of comparing break statements to avoid the situation:
* <pre><code>
* while(a) {
* if(foo()) break;
* while(b) {
* if(foo()) break;
* }
* }
* </code></pre>
* it isn't equivalent to:
* <pre><code>
* do(a) {
* if(foo()) break;
* }
* </code></pre>
*/
private static class BreakTrackingEquivalenceChecker extends TrackingEquivalenceChecker {
@Override
protected Match breakStatementsMatch(@NotNull PsiBreakStatement statement1, @NotNull PsiBreakStatement statement2) {
return !isParentFor(getFirstLoop(statement1), getFirstLoop(statement2))
? super.breakStatementsMatch(statement1, statement2)
: EXACT_MISMATCH;
}
@Nullable
private static PsiElement getFirstLoop(PsiElement element) {
while ((element = element.getParent()) != null) {
if (element instanceof PsiLoopStatement) return element;
}
return element;
}
private static boolean isParentFor(@Nullable PsiElement element1, @Nullable PsiElement element2) {
if (element1 == null || element2 == null) return false;
if (element1.equals(element2)) return false;
PsiElement pinedElement1 = element1;
while ((element1 = element1.getParent()) != null) {
if (element1.equals(element2)) return true;
}
while ((element2 = element2.getParent()) != null) {
if (pinedElement1.equals(element2)) return true;
}
return false;
}
}
}
}
@@ -1,79 +0,0 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ipp.whileloop;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.psi.*;
import com.siyeh.IntentionPowerPackBundle;
import com.siyeh.ig.PsiReplacementUtil;
import com.siyeh.ig.psiutils.BoolUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ipp.base.MCIntention;
import com.siyeh.ipp.base.PsiElementPredicate;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public class ReplaceWhileLoopWithDoWhileLoopIntention extends MCIntention {
@Override
public @NotNull String getFamilyName() {
return IntentionPowerPackBundle.message("replace.while.loop.with.do.while.loop.intention.family.name");
}
@Override
public @IntentionName @NotNull String getTextForElement(@NotNull PsiElement element) {
return IntentionPowerPackBundle.message("replace.while.loop.with.do.while.loop.intention.name");
}
@Override
@NotNull
protected PsiElementPredicate getElementPredicate() {
return new WhileLoopPredicate();
}
@Override
protected void processIntention(@NotNull PsiElement element) {
final PsiWhileStatement whileStatement = (PsiWhileStatement)element.getParent();
if (whileStatement == null) {
return;
}
final PsiStatement body = whileStatement.getBody();
final PsiExpression condition = whileStatement.getCondition();
final boolean infiniteLoop = BoolUtils.isTrue(condition);
@NonNls final StringBuilder doWhileStatementText = new StringBuilder();
CommentTracker tracker = new CommentTracker();
if (!infiniteLoop) {
doWhileStatementText.append("if(");
if (condition != null) {
doWhileStatementText.append(tracker.text(condition));
}
doWhileStatementText.append(") {\n");
}
if (body instanceof PsiBlockStatement blockStatement) {
doWhileStatementText.append("do {");
final PsiCodeBlock codeBlock = blockStatement.getCodeBlock();
final PsiElement[] children = codeBlock.getChildren();
if (children.length > 2) {
for (int i = 1; i < children.length - 1; i++) {
final PsiElement child = children[i];
doWhileStatementText.append(tracker.text(child));
}
}
doWhileStatementText.append('}');
}
else if (body != null) {
doWhileStatementText.append("do ").append(tracker.text(body)).append('\n');
}
doWhileStatementText.append("while(");
if (condition != null) {
doWhileStatementText.append(tracker.text(condition));
}
doWhileStatementText.append(");");
if (!infiniteLoop) {
doWhileStatementText.append("\n}");
}
PsiReplacementUtil.replaceStatement(whileStatement, doWhileStatementText.toString(), tracker);
}
}
@@ -0,0 +1,26 @@
<html>
<body>
<p>
Reports <code>while</code> loops that could be more effectively written as <code>do-while</code> loops.
The focus is on cases where the entire body of the <code>while</code> loop is executed before the condition is checked,
leading to potential redundancy in code execution.
</p>
<p><b>Example:</b></p>
<pre><code>
foo();
while (x) {
foo();
}
</code></pre>
<p>Can be replaced with:</p>
<pre><code>
do {
foo();
} while (x);
</code></pre>
<!-- tooltip end -->
<p>
<small>New in 2024.1</small>
</p>
</body>
</html>
@@ -1,10 +0,0 @@
package com.siyeh.ipp.whileloop.replace_while_with_do_while_loop;
class InfiniteLoop {
void m() {
while<caret>((true)) {
System.out.println(1);
}
}
}
@@ -1,10 +0,0 @@
package com.siyeh.ipp.whileloop.replace_while_with_do_while_loop;
class InfiniteLoop {
void m() {
do {
System.out.println(1);
} while ((true));
}
}
@@ -1,11 +0,0 @@
class NoBraces {
void m() {
while<caret>//after while
(b(/*inside call*/)) //before body
System.out.println();
}
boolean b() {
return true;
}
}
@@ -1,14 +0,0 @@
class NoBraces {
void m() {
//after while
//before body
if (b(/*inside call*/)) {
do System.out.println();
while (b(/*inside call*/));
}
}
boolean b() {
return true;
}
}
@@ -1,14 +0,0 @@
package com.siyeh.ipp.whileloop.replace_while_with_do_while_loop;
class Regular {
void m() {
<caret>while(b()) {
System.out.println(1);
}
}
boolean b() {
return true;
}
}
@@ -1,16 +0,0 @@
package com.siyeh.ipp.whileloop.replace_while_with_do_while_loop;
class Regular {
void m() {
if (b()) {
do {
System.out.println(1);
} while (b());
}
}
boolean b() {
return true;
}
}
@@ -0,0 +1,335 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.whileloop;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
public class WhileCanBeDoWhileInspectionTest extends LightJavaCodeInsightFixtureTestCase {
public void testReplaceSingleStatement() {
doTest("""
class DoWhileClass {
void test() {
System.out.println(1);
while<caret>(condition()) {
System.out.println(1);
}
}
boolean condition() { return true; }
}""", """
class DoWhileClass {
void test() {
do {
System.out.println(1);
} while (condition());
}
boolean condition() { return true; }
}""");
}
public void testReplaceSingleStatementWithoutCodeBlock() {
doTest("""
class DoWhileClass {
void test() {
System.out.println(1);
while<caret>(condition())
System.out.println(1);
}
boolean condition() { return true; }
}""", """
class DoWhileClass {
void test() {
do System.out.println(1);
while (condition());
}
boolean condition() { return true; }
}""");
}
public void testReplaceCodeBlock() {
doTest("""
class DoWhileClass {
void test() {
int sum = 0;
{
sum += 1;
print(sum);
int a = 2;
sum = sum * a;
}
while<caret>(sum < 100) {
sum += 1;
print(sum);
int b = 2;
sum = sum * b;
}
}
void print(int a) {
System.out.println(a);
}
}""", """
class DoWhileClass {
void test() {
int sum = 0;
do {
sum += 1;
print(sum);
int b = 2;
sum = sum * b;
} while (sum < 100);
}
void print(int a) {
System.out.println(a);
}
}""");
}
public void testReplaceCodeWithComments() {
doTest("""
class DoWhileClass {
void test() {
int sum = 0;
// comment 0
sum += 1;
// comment 1
print(sum);
int a = 2;
sum = sum * a;
// comment 2
while<caret>(sum < 100) {
// comment 3
sum += 1;
print(sum);
int b = 2;
sum = sum * b;
}
}
void print(int a) {
System.out.println(a);
}
}""", """
class DoWhileClass {
void test() {
int sum = 0;
// comment 0
// comment 2
do {
// comment 3
sum += 1;
print(sum);
int b = 2;
sum = sum * b;
} while (sum < 100);
}
void print(int a) {
System.out.println(a);
}
}""");
}
public void testDifferentBreaks() {
doTest("""
class DoWhileClass {
void test() {
for(int i = 0; i < 10; i++) {
int j = i;
if(condition()) break;
j--;
while<caret>(j > 4) {
if(condition()) break;
j--;
}
}
}
boolean condition() { return true; }
}""", """
class DoWhileClass {
void test() {
for(int i = 0; i < 10; i++) {
int j = i;
if(condition()) break;
j--;
if (j > 4) {
do {
if (condition()) break;
j--;
} while (j > 4);
}
}
}
boolean condition() { return true; }
}""");
}
public void testEqBreaks() {
doTest("""
class DoWhileClass {
void test() {
for(int i = 0; i < 10; i++) {
int j = i;
for(int k = 0; k < 2; k++) if(condition()) break;
j--;
while<caret>(j > 4) {
for(int k = 0; k < 2; k++) if(condition()) break;
j--;
}
}
}
boolean condition() { return true; }
}""", """
class DoWhileClass {
void test() {
for(int i = 0; i < 10; i++) {
int j = i;
do {
for (int k = 0; k < 2; k++) if (condition()) break;
j--;
} while (j > 4);
}
}
boolean condition() { return true; }
}""");
}
public void testTrue() {
doTest("""
class DoWhileClass {
void test() {
int sum = 0;
sum += 1;
System.out.println("sum");
while<caret>(true) {
sum += 1;
System.out.println("sum");
}
}
}""", """
class DoWhileClass {
void test() {
int sum = 0;
do {
sum += 1;
System.out.println("sum");
} while (true);
}
}""");
}
public void testInfiniteLoop() {
doTest("""
class DoWhileClass {
void test() {
while<caret>((true)) {
System.out.println(1);
}
}
}""", """
class DoWhileClass {
void test() {
do {
System.out.println(1);
} while ((true));
}
}""");
}
public void testNoBraces() {
doTest("""
class DoWhileClass {
void test() {
while<caret>//after while
(b(/*inside call*/)) //before body
System.out.println();
}
boolean b() { return true; }
}""", """
class DoWhileClass {
void test() {
//after while
//before body
if (b(/*inside call*/)) {
do System.out.println();
while (b(/*inside call*/));
}
}
boolean b() { return true; }
}""");
}
public void testRegular() {
doTest("""
class DoWhileClass {
void test() {
<caret>while(b()) {
System.out.println(1);
};
}
boolean b() { return true; }
}""", """
class DoWhileClass {
void test() {
if (b()) {
do {
System.out.println(1);
} while (b());
}
;
}
boolean b() { return true; }
}""");
}
public void testDifferentComments() {
testHighlighting("""
class DoWhileClass {
void test() {
int sum = 0;
sum += 1;
// comment 1
System.out.println("sum");
<info descr="Replace 'while' with 'do while'">while</info>(true) {
sum += 1;
// comment 2
System.out.println("sum");
}
}
}""");
}
public void testEqComments() {
testHighlighting("""
class DoWhileClass {
void test() {
int sum = 0;
sum += 1;
// comment
System.out.println("sum");
<weak_warning descr="Replace 'while' with 'do while'">while</weak_warning>(true) {
sum += 1;
// comment
System.out.println("sum");
}
}
}""");
}
private void doTest(@NotNull @Language("Java") String before, @NotNull @Language("Java") String after) {
myFixture.configureByText("DoWhile.java", before);
myFixture.enableInspections(new WhileCanBeDoWhileInspection());
myFixture.launchAction(myFixture.findSingleIntention("Replace 'while' with 'do while'"));
myFixture.checkResult(after);
}
private void testHighlighting(@NotNull @Language("Java") String code) {
myFixture.configureByText("DoWhile.java", code);
myFixture.enableInspections(new WhileCanBeDoWhileInspection());
myFixture.testHighlighting(true, true, true);
}
}
@@ -1,39 +0,0 @@
/*
* Copyright 2000-2013 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.siyeh.ipp.whileloop;
import com.siyeh.IntentionPowerPackBundle;
import com.siyeh.ipp.IPPTestCase;
/**
* @author Bas Leijdekkers
*/
public class ReplaceWhileLoopWithDoWhileLoopIntentionTest extends IPPTestCase {
public void testInfiniteLoop() { doTest(); }
public void testRegular() { doTest(); }
public void testNoBraces() { doTest(); }
@Override
protected String getRelativePath() {
return "whileloop/replace_while_with_do_while_loop";
}
@Override
protected String getIntentionName() {
return IntentionPowerPackBundle.message("replace.while.loop.with.do.while.loop.intention.name");
}
}