IDEA-209056 Action to transform method with multiple returns into the method with single exit point

First implementation
This commit is contained in:
Tagir Valeev
2019-03-19 15:02:38 +07:00
parent 3e401e1a95
commit 5b3d311505
54 changed files with 1518 additions and 1 deletions
@@ -1277,6 +1277,10 @@
<className>com.intellij.codeInsight.intention.impl.SplitSwitchBranchWithSeveralCaseValuesAction</className>
<category>Java/Control Flow</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.singlereturn.ConvertToSingleReturnAction</className>
<category>Java/Control Flow</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.WrapWithUnmodifiableAction</className>
<category>Java/Other</category>
@@ -0,0 +1,106 @@
// Copyright 2000-2019 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.singlereturn;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiTypesUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.intellij.util.ObjectUtils.tryCast;
public class ConvertToSingleReturnAction extends PsiElementBaseIntentionAction {
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
PsiCodeBlock block = findBlock(element);
if (block == null) return;
PsiType returnType = PsiTypesUtil.getMethodReturnType(block);
if (returnType == null) return;
PsiCodeBlock copy = (PsiCodeBlock)block.copy();
process(project, copy, returnType, FinishMarker.defineFinishMarker(block, returnType));
CodeStyleManager.getInstance(project).reformat(block.replace(copy));
}
private static void process(@NotNull Project project,
PsiCodeBlock block,
PsiType returnType,
FinishMarker marker) {
ExitContext exitContext = new ExitContext(block, returnType, marker);
while (true) {
ProgressManager.checkCanceled();
PsiReturnStatement returnStatement = getNonTerminalReturn(block);
if (returnStatement == null) break;
ReturnReplacementContext.replaceSingleReturn(project, block, exitContext, returnStatement);
}
exitContext.declareVariables();
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
PsiCodeBlock block = findBlock(element);
if (block == null) return false;
PsiType returnType = PsiTypesUtil.getMethodReturnType(block);
return returnType != null && getNonTerminalReturn(block) != null;
}
@NotNull
@Override
public String getText() {
return getFamilyName();
}
@Nullable
private static PsiCodeBlock findBlock(PsiElement element) {
PsiParameterListOwner owner = PsiTreeUtil.getParentOfType(element, PsiParameterListOwner.class, false, PsiCodeBlock.class);
if (owner == null) return null;
return tryCast(owner.getBody(), PsiCodeBlock.class);
}
private static PsiReturnStatement getNonTerminalReturn(@NotNull PsiCodeBlock block) {
PsiStatement lastStatement = ArrayUtil.getLastElement(block.getStatements());
if (lastStatement == null) return null;
class Visitor extends JavaRecursiveElementWalkingVisitor {
private PsiReturnStatement myReturnStatement;
@Override
public void visitReturnStatement(PsiReturnStatement statement) {
super.visitReturnStatement(statement);
if (lastStatement != statement) {
myReturnStatement = statement;
stopWalking();
}
}
@Override
public void visitExpression(PsiExpression expression) {}
@Override
public void visitLambdaExpression(PsiLambdaExpression expression) {}
@Override
public void visitClass(PsiClass aClass) {}
}
Visitor visitor = new Visitor();
block.accept(visitor);
return visitor.myReturnStatement;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getFamilyName() {
return CodeInsightBundle.message("intention.convert.to.single.return.name");
}
}
@@ -0,0 +1,152 @@
// Copyright 2000-2019 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.singlereturn;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.util.PsiTypesUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.BoolUtils;
import com.siyeh.ig.psiutils.EquivalenceChecker;
import com.siyeh.ig.psiutils.VariableNameGenerator;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
import static java.util.Objects.requireNonNull;
/**
* Tracks method exit strategy and additional variables which could be necessary for single-return conversion
*/
class ExitContext {
private final @NotNull PsiType myReturnType;
private final @NotNull FinishMarker.FinishMarkerType myFinishMarkerType;
private String myFinishedVariable;
private final @NotNull PsiCodeBlock myBlock;
private final @NotNull String myReturnVariable;
private final @NotNull PsiElementFactory myFactory;
boolean myReturnVariableUsed = false;
PsiExpression myReturnVariableDefaultValue;
ExitContext(@NotNull PsiCodeBlock block, @NotNull PsiType returnType, @NotNull FinishMarker marker) {
myBlock = block;
myFactory = JavaPsiFacade.getElementFactory(block.getProject());
myReturnType = returnType;
myReturnVariable =
new VariableNameGenerator(block, VariableKind.LOCAL_VARIABLE).byName("result", "res").byType(returnType).generate(false);
myReturnVariableDefaultValue = marker.myDefaultValue;
if (myReturnVariableDefaultValue != null && myReturnVariableDefaultValue.isPhysical()) {
myReturnVariableDefaultValue = (PsiExpression)myReturnVariableDefaultValue.copy();
}
myFinishMarkerType = marker.myType;
}
String generateExitCondition() {
switch (myFinishMarkerType) {
case BOOLEAN_FALSE:
return "!" + myReturnVariable;
case BOOLEAN_TRUE:
return myReturnVariable;
case VALUE_EQUAL:
return myReturnVariable + "==" + myReturnVariableDefaultValue.getText();
case VALUE_NON_EQUAL:
return myReturnVariable + "!=" + myReturnVariableDefaultValue.getText();
default:
assert myFinishedVariable != null;
return myFinishedVariable;
}
}
String getNonExitCondition() {
switch (myFinishMarkerType) {
case BOOLEAN_FALSE:
return myReturnVariable;
case BOOLEAN_TRUE:
return "!" + myReturnVariable;
case VALUE_EQUAL:
return myReturnVariable + "!=" + myReturnVariableDefaultValue.getText();
case VALUE_NON_EQUAL:
return myReturnVariable + "==" + myReturnVariableDefaultValue.getText();
default:
assert myFinishedVariable != null;
return "!" + myFinishedVariable;
}
}
void registerReturnValue(PsiExpression value, List<String> replacements) {
myReturnVariableUsed = true;
if (FinishMarker.canMoveToStart(value) &&
(myReturnVariableDefaultValue == null ||
EquivalenceChecker.getCanonicalPsiEquivalence().expressionsAreEquivalent(myReturnVariableDefaultValue, value))) {
myReturnVariableDefaultValue = (PsiExpression)value.copy();
}
else {
replacements.add(myReturnVariable + "=" + value.getText() + ";");
}
}
void register(List<String> replacements) {
if (myFinishMarkerType != FinishMarker.FinishMarkerType.SEPARATE_VAR) return;
if (myFinishedVariable == null) {
myFinishedVariable =
new VariableNameGenerator(myBlock, VariableKind.LOCAL_VARIABLE).byName("finished", "completed").generate(false);
}
String firstItem = ContainerUtil.getFirstItem(replacements);
String assignment = myFinishedVariable + "=true;";
if (!assignment.equals(firstItem)) {
replacements.add(0, assignment);
}
}
void declareVariables() {
if (myFinishedVariable != null) {
PsiJavaToken start = requireNonNull(myBlock.getLBrace());
PsiExpression initializer = myFactory.createExpressionFromText("false", null);
PsiDeclarationStatement declaration =
myFactory.createVariableDeclarationStatement(myFinishedVariable, PsiType.BOOLEAN, initializer);
myBlock.addAfter(declaration, start);
}
if (myReturnVariableUsed) {
PsiJavaToken start = requireNonNull(myBlock.getLBrace());
if (myReturnVariableDefaultValue == null && myFinishedVariable != null) {
myReturnVariableDefaultValue = myFactory.createExpressionFromText(PsiTypesUtil.getDefaultValueOfType(myReturnType), null);
}
PsiDeclarationStatement declaration =
myFactory.createVariableDeclarationStatement(myReturnVariable, myReturnType, myReturnVariableDefaultValue);
myBlock.addAfter(declaration, start);
PsiJavaToken end = requireNonNull(myBlock.getRBrace());
myBlock.addBefore(myFactory.createStatementFromText("return " + myReturnVariable + ";", myBlock), end);
}
}
public boolean isFinishCondition(PsiStatement statement) {
if (!(statement instanceof PsiIfStatement)) return false;
PsiExpression condition = ((PsiIfStatement)statement).getCondition();
if (condition == null) return false;
if (BoolUtils.isNegation(condition)) {
condition = BoolUtils.getNegated(condition);
}
if (condition instanceof PsiBinaryExpression) {
condition = ((PsiBinaryExpression)condition).getLOperand();
}
if (!(condition instanceof PsiReferenceExpression)) return false;
PsiReferenceExpression ref = (PsiReferenceExpression)condition;
return ref.getQualifierExpression() == null &&
(Objects.equals(myFinishedVariable, ref.getReferenceName()) ||
Objects.equals(myReturnVariable, ref.getReferenceName()));
}
/**
* @param statement statement to check
* @return true if given statement is a return statement which returns registered default value, so it could be fully removed
*/
boolean isDefaultReturn(PsiStatement statement) {
if (myReturnVariableDefaultValue != null && statement instanceof PsiReturnStatement) {
PsiReturnStatement returnStatement = (PsiReturnStatement)statement;
return EquivalenceChecker.getCanonicalPsiEquivalence()
.expressionsAreEquivalent(myReturnVariableDefaultValue, returnStatement.getReturnValue()) &&
!FinishMarker.mayNeedMarker(returnStatement, myBlock);
}
return false;
}
}
@@ -0,0 +1,278 @@
// Copyright 2000-2019 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.singlereturn;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
import com.intellij.codeInspection.dataFlow.CommonDataflow;
import com.intellij.codeInspection.dataFlow.NullabilityUtil;
import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ArrayUtil;
import com.siyeh.ig.psiutils.ControlFlowUtils;
import com.siyeh.ig.psiutils.ExpressionUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static com.intellij.util.ObjectUtils.NULL;
import static com.intellij.util.ObjectUtils.tryCast;
import static java.util.Objects.requireNonNull;
/**
* Represents a way to indicate whether method execution is already finished
*/
class FinishMarker {
/**
* Type of finish marker
*/
final @NotNull FinishMarkerType myType;
/**
* Sentinel value
*/
final @Nullable PsiExpression myDefaultValue;
private FinishMarker(@NotNull FinishMarkerType type, @Nullable PsiExpression value) {
myType = type;
myDefaultValue = value;
}
/**
* @param block method body (must be physical as CommonDataflow will be queried)
* @param returnType method return type
* @return a FinishMarker which is suitable for given method
*/
static FinishMarker defineFinishMarker(@NotNull PsiCodeBlock block, @NotNull PsiType returnType) {
List<PsiReturnStatement> returns = findReturns(block);
boolean mayNeedMarker = mayNeedMarker(returns, block);
return defineFinishMarker(block, returns, returnType, mayNeedMarker, JavaPsiFacade.getElementFactory(block.getProject()));
}
private static boolean mayNeedMarker(List<PsiReturnStatement> returns, PsiCodeBlock block) {
for (PsiReturnStatement returnStatement : returns) {
if (mayNeedMarker(returnStatement, block)) return true;
}
return false;
}
/**
* Checks whether we may need a marker value to indicate premature exit from given return statement.
*
* @param returnStatement return statement to check
* @param block method body (ancestor of return statement).
* @return false if it's possible to transform the code removing given return statement without introducing a marker;
* true if marker might be necessary.
*/
static boolean mayNeedMarker(PsiReturnStatement returnStatement, PsiCodeBlock block) {
PsiElement parent = returnStatement.getParent();
if (parent instanceof PsiCodeBlock) {
PsiElement grandParent = parent.getParent();
if (grandParent instanceof PsiStatement) {
parent = grandParent.getParent();
}
else {
return parent != block;
}
}
if (!(parent instanceof PsiStatement)) return true;
PsiStatement currentContext = (PsiStatement)parent;
PsiStatement loopOrSwitch = PsiTreeUtil.getNonStrictParentOfType(currentContext, PsiLoopStatement.class, PsiSwitchStatement.class);
if (loopOrSwitch != null && PsiTreeUtil.isAncestor(block, loopOrSwitch, true)) {
currentContext = loopOrSwitch;
}
else {
while (true) {
PsiElement ifParent = currentContext.getParent();
if (!(ifParent instanceof PsiCodeBlock)) break;
if (!(ifParent.getParent() instanceof PsiStatement)) {
return ifParent != block;
}
currentContext = (PsiStatement)ifParent.getParent();
if (!(currentContext instanceof PsiBlockStatement) ||
!(currentContext.getParent() instanceof PsiIfStatement) ||
ControlFlowUtils.codeBlockMayCompleteNormally((PsiCodeBlock)ifParent)) {
break;
}
currentContext = (PsiStatement)currentContext.getParent();
}
}
while (true) {
PsiElement contextParent = currentContext.getParent();
if (contextParent instanceof PsiCodeBlock) {
PsiStatement[] contextStatements = ((PsiCodeBlock)contextParent).getStatements();
int pos = ArrayUtil.indexOf(contextStatements, currentContext);
assert pos >= 0;
if (pos < contextStatements.length - 1) return true;
if (contextParent == block) return false;
if (!(contextParent.getParent() instanceof PsiStatement)) return true;
currentContext = (PsiStatement)contextParent.getParent();
}
else if (contextParent instanceof PsiIfStatement || contextParent instanceof PsiLabeledStatement) {
currentContext = (PsiStatement)contextParent;
}
else {
return true;
}
}
}
private static FinishMarker defineFinishMarker(PsiCodeBlock block, List<PsiReturnStatement> returns, PsiType returnType,
boolean mayNeedMarker, PsiElementFactory factory) {
if (PsiType.VOID.equals(returnType)) {
return new FinishMarker(FinishMarkerType.SEPARATE_VAR, null);
}
PsiReturnStatement terminalReturn = tryCast(ArrayUtil.getLastElement(block.getStatements()), PsiReturnStatement.class);
List<PsiExpression> nonTerminalReturns = StreamEx.of(returns).without(terminalReturn)
.map(PsiReturnStatement::getReturnValue)
.map(PsiUtil::skipParenthesizedExprDown).toList();
if (nonTerminalReturns.size() == 0) {
return new FinishMarker(FinishMarkerType.SEPARATE_VAR, null);
}
Set<Object> nonTerminalReturnValues = StreamEx.of(nonTerminalReturns)
.map(val -> val instanceof PsiLiteralExpression ? ((PsiLiteralExpression)val).getValue() : NULL)
.toSet();
if (!mayNeedMarker) {
if (nonTerminalReturnValues.size() == 1 && nonTerminalReturnValues.iterator().next() != NULL) {
return new FinishMarker(FinishMarkerType.SEPARATE_VAR, nonTerminalReturns.iterator().next());
}
return new FinishMarker(FinishMarkerType.SEPARATE_VAR, null);
}
if (PsiType.BOOLEAN.equals(returnType)) {
if (nonTerminalReturnValues.size() == 1) {
Object value = nonTerminalReturnValues.iterator().next();
if (value instanceof Boolean) {
boolean boolReturn = (boolean)value;
FinishMarkerType markerType = boolReturn ? FinishMarkerType.BOOLEAN_TRUE : FinishMarkerType.BOOLEAN_FALSE;
return new FinishMarker(markerType, factory.createExpressionFromText(String.valueOf(!boolReturn), null));
}
}
}
if (PsiType.INT.equals(returnType) || PsiType.LONG.equals(returnType)) {
return getMarkerForIntegral(nonTerminalReturns, terminalReturn, returnType, factory);
}
if (!(returnType instanceof PsiPrimitiveType)) {
if (StreamEx.of(nonTerminalReturns).map(ret -> NullabilityUtil.getExpressionNullability(ret, true))
.allMatch(Nullability.NOT_NULL::equals)) {
return new FinishMarker(FinishMarkerType.VALUE_NON_EQUAL, factory.createExpressionFromText("null", null));
}
}
if (terminalReturn != null) {
PsiExpression value = terminalReturn.getReturnValue();
if (value != null && canMoveToStart(value)) {
return new FinishMarker(FinishMarkerType.SEPARATE_VAR, value);
}
}
return new FinishMarker(FinishMarkerType.SEPARATE_VAR, null);
}
@NotNull
private static FinishMarker getMarkerForIntegral(List<PsiExpression> nonTerminalReturns,
PsiReturnStatement terminalReturn,
PsiType returnType, PsiElementFactory factory) {
boolean isLong = PsiType.LONG.equals(returnType);
LongRangeSet fullSet = requireNonNull(LongRangeSet.fromType(returnType));
LongRangeSet set = nonTerminalReturns.stream()
.map(CommonDataflow::getExpressionRange)
.map(range -> range == null ? fullSet : range)
.reduce(LongRangeSet::unite)
.orElse(fullSet);
if (!set.isEmpty() && !set.contains(fullSet)) {
PsiExpression terminalReturnValue = terminalReturn == null ? null : terminalReturn.getReturnValue();
LongRangeSet terminal = CommonDataflow.getExpressionRange(terminalReturnValue);
Long point;
if (terminal != null) {
point = terminal.getConstantValue();
if (point != null && !set.contains(point)) {
PsiExpression defValue = canMoveToStart(terminalReturnValue) ?
(PsiExpression)terminalReturnValue.copy() :
factory.createExpressionFromText(point + (isLong ? "L" : ""), null);
return new FinishMarker(FinishMarkerType.VALUE_NON_EQUAL, defValue);
}
}
long[] candidates = {0, 1, -1, fullSet.min(), fullSet.max()};
point = null;
for (long candidate : candidates) {
if (!set.contains(candidate)) {
point = candidate;
break;
}
}
if (point != null) {
String text = point == Integer.MIN_VALUE ? "java.lang.Integer.MIN_VALUE" :
point == Integer.MAX_VALUE ? "java.lang.Integer.MAX_VALUE" :
point == Long.MIN_VALUE ? "java.lang.Long.MIN_VALUE" :
point == Long.MAX_VALUE ? "java.lang.Long.MAX_VALUE" :
String.valueOf(point);
return new FinishMarker(FinishMarkerType.VALUE_NON_EQUAL, factory.createExpressionFromText(text, null));
}
}
return new FinishMarker(FinishMarkerType.SEPARATE_VAR, null);
}
static boolean canMoveToStart(PsiExpression value) {
if (!ExpressionUtils.isSafelyRecomputableExpression(value)) return false;
PsiReferenceExpression ref = tryCast(PsiUtil.skipParenthesizedExprDown(value), PsiReferenceExpression.class);
if (ref != null && !ref.isQualified()) {
PsiVariable target = tryCast(ref.resolve(), PsiVariable.class);
if (target instanceof PsiLocalVariable) return false;
if (target instanceof PsiParameter) {
PsiElement block = PsiUtil.getVariableCodeBlock(target, null);
return block != null && HighlightControlFlowUtil.isEffectivelyFinal(target, block, null);
}
}
return true;
}
private static List<PsiReturnStatement> findReturns(PsiCodeBlock block) {
List<PsiReturnStatement> result = new ArrayList<>();
block.accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitReturnStatement(PsiReturnStatement statement) {
super.visitReturnStatement(statement);
result.add(statement);
}
@Override
public void visitExpression(PsiExpression expression) {}
@Override
public void visitLambdaExpression(PsiLambdaExpression expression) {}
@Override
public void visitClass(PsiClass aClass) {}
});
return result;
}
/**
* Type of finish marker
*/
enum FinishMarkerType {
/**
* If result boolean variable is true, then the method execution is finished
*/
BOOLEAN_TRUE,
/**
* If result boolean variable is false, then the method execution is finished
*/
BOOLEAN_FALSE,
/**
* If result variable is equal to sentinel, then the method execution is finished
*/
VALUE_EQUAL,
/**
* If result variable is not equal to sentinel, then the method execution is finished
*/
VALUE_NON_EQUAL,
/**
* Separate boolean variable is used to indicate whether the method execution is finished.
* This value also used if it was detected that no finish marker is actually necessary
*/
SEPARATE_VAR
}
}
@@ -0,0 +1,285 @@
// Copyright 2000-2019 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.singlereturn;
import com.intellij.codeInsight.BlockUtils;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.BoolUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.ControlFlowUtils;
import com.siyeh.ig.psiutils.SideEffectChecker;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static com.intellij.util.ObjectUtils.tryCast;
import static java.util.Objects.requireNonNull;
/**
* Performs replacement of single return statement as the part of {@link ConvertToSingleReturnAction}.
*/
class ReturnReplacementContext {
private final Project myProject;
private final PsiElementFactory myFactory;
private final PsiCodeBlock myBlock;
private final ExitContext myExitContext;
private PsiReturnStatement myReturnStatement;
private final List<String> myReplacements = new ArrayList<>();
private ReturnReplacementContext(Project project,
PsiCodeBlock block,
ExitContext context,
PsiReturnStatement statement) {
myProject = project;
myFactory = JavaPsiFacade.getElementFactory(project);
myBlock = block;
myExitContext = context;
myReturnStatement = statement;
}
private void process() {
PsiExpression value = myReturnStatement.getReturnValue();
if (value != null) {
myExitContext.registerReturnValue(value, myReplacements);
}
PsiStatement currentContext = goUp();
while (currentContext != null) {
currentContext = advance(currentContext);
}
replace();
}
@NotNull
private PsiStatement goUp() {
PsiElement parent = myReturnStatement.getParent();
if (parent instanceof PsiCodeBlock) {
PsiElement grandParent = parent.getParent();
if (!(grandParent instanceof PsiSwitchStatement)) {
PsiStatement[] statements = ((PsiCodeBlock)parent).getStatements();
boolean afterReturn = false;
for (PsiStatement statement : statements) {
if (statement == myReturnStatement) {
afterReturn = true;
}
else if (afterReturn) {
// Unreachable statements after return (compilation error): remove them
new CommentTracker().deleteAndRestoreComments(statement);
}
}
}
if (grandParent instanceof PsiBlockStatement || grandParent instanceof PsiTryStatement ||
grandParent instanceof PsiSwitchStatement) {
parent = grandParent.getParent();
}
else if (parent != myBlock) {
throw new RuntimeExceptionWithAttachments("Unexpected structure: " + grandParent.getClass(),
new Attachment("body.txt", myBlock.getText()),
new Attachment("context.txt", grandParent.getText()));
}
}
if (!(parent instanceof PsiStatement)) {
throw new RuntimeExceptionWithAttachments("Unexpected structure: " + parent.getClass(),
new Attachment("body.txt", myBlock.getText()),
new Attachment("context.txt", parent.getText()));
}
PsiStatement currentContext = (PsiStatement)parent;
PsiStatement loopOrSwitch = PsiTreeUtil.getNonStrictParentOfType(currentContext, PsiLoopStatement.class, PsiSwitchStatement.class);
if (loopOrSwitch != null && PsiTreeUtil.isAncestor(myBlock, loopOrSwitch, true)) {
myReplacements.add("break;");
return loopOrSwitch;
}
while (true) {
if (currentContext instanceof PsiIfStatement) {
PsiIfStatement ifStatement = (PsiIfStatement)currentContext;
boolean inThen = PsiTreeUtil.isAncestor(ifStatement.getThenBranch(), myReturnStatement, false);
PsiElement ifParent = currentContext.getParent();
if (ifParent instanceof PsiCodeBlock) {
PsiCodeBlock resultBlock = swallowTail(currentContext, ifStatement, inThen, (PsiCodeBlock)ifParent);
if (resultBlock != null &&
!ControlFlowUtils.codeBlockMayCompleteNormally(resultBlock) &&
ifParent.getParent() instanceof PsiBlockStatement &&
ifParent.getParent().getParent() instanceof PsiIfStatement) {
currentContext = (PsiStatement)ifParent.getParent().getParent();
continue;
}
}
}
return currentContext;
}
}
@Nullable
private PsiStatement advance(PsiStatement currentContext) {
PsiElement contextParent = currentContext.getParent();
if (contextParent instanceof PsiLoopStatement) {
Object mark = new Object();
PsiTreeUtil.mark(myReturnStatement, mark);
currentContext = BlockUtils.expandSingleStatementToBlockStatement(currentContext);
contextParent = currentContext.getParent();
myReturnStatement = (PsiReturnStatement)requireNonNull(PsiTreeUtil.releaseMark(currentContext, mark));
}
if (contextParent instanceof PsiCodeBlock) {
PsiElement[] tail = extractTail(currentContext, (PsiCodeBlock)contextParent);
PsiStatement loopOrSwitch = PsiTreeUtil.getParentOfType(currentContext, PsiLoopStatement.class, PsiSwitchStatement.class);
if (loopOrSwitch != null && PsiTreeUtil.isAncestor(myBlock, loopOrSwitch, true)) {
myExitContext.register(myReplacements);
String exitStatement = "if(" + myExitContext.generateExitCondition() + ") break;";
contextParent.addAfter(myFactory.createStatementFromText(exitStatement, currentContext), currentContext);
currentContext = loopOrSwitch;
return currentContext;
}
List<PsiStatement> statements = StreamEx.of(tail).select(PsiStatement.class).toList();
if (!statements.isEmpty()) {
PsiStatement statement = statements.get(0);
if (statements.size() == 1 && myExitContext.isDefaultReturn(statement)) {
new CommentTracker().deleteAndRestoreComments(statement);
}
else {
myExitContext.register(myReplacements);
if (!myExitContext.isFinishCondition(statement)) {
String conditionalBlock = "if(" + myExitContext.getNonExitCondition() + ") {}";
PsiIfStatement ifStatement = (PsiIfStatement)myFactory.createStatementFromText(conditionalBlock, currentContext);
PsiCodeBlock ifBlock = requireNonNull(((PsiBlockStatement)requireNonNull(ifStatement.getThenBranch())).getCodeBlock());
PsiJavaToken lBrace = requireNonNull(ifBlock.getLBrace());
PsiElement tailStart = ArrayUtil.getFirstElement(tail);
PsiElement tailEnd = ArrayUtil.getLastElement(tail);
ifBlock.addRangeAfter(tailStart, tailEnd, lBrace);
contextParent.deleteChildRange(tailStart, tailEnd);
contextParent.addAfter(ifStatement, currentContext);
}
}
}
if (contextParent == myBlock) {
currentContext = null;
}
else if (contextParent.getParent() instanceof PsiStatement) {
currentContext = (PsiStatement)contextParent.getParent();
}
else {
throw new RuntimeExceptionWithAttachments("Unexpected structure: " + contextParent.getParent().getClass(),
new Attachment("body.txt", myBlock.getText()),
new Attachment("context.txt", contextParent.getText()));
}
}
else if (contextParent instanceof PsiIfStatement || contextParent instanceof PsiLabeledStatement) {
currentContext = (PsiStatement)contextParent;
}
else {
throw new RuntimeExceptionWithAttachments("Unexpected structure: " + contextParent.getClass(),
new Attachment("body.txt", myBlock.getText()),
new Attachment("context.txt", contextParent.getText()));
}
return currentContext;
}
@NotNull
private static PsiElement[] extractTail(PsiStatement current, PsiCodeBlock block) {
PsiElement[] children = block.getChildren();
int pos = ArrayUtil.indexOf(children, current);
assert pos >= 0;
PsiElement rBrace = block.getRBrace();
int endPos = rBrace == null ? children.length : ArrayUtil.lastIndexOf(children, rBrace);
assert endPos >= pos;
return Arrays.copyOfRange(children, pos + 1, endPos);
}
private PsiCodeBlock swallowTail(PsiStatement currentContext,
PsiIfStatement ifStatement,
boolean inThen, PsiCodeBlock ifParent) {
PsiElement[] tail = extractTail(currentContext, ifParent);
if (Arrays.stream(tail).noneMatch(PsiStatement.class::isInstance)) return null;
PsiBlockStatement blockForTail = getBlockFromIf(ifStatement, inThen);
PsiCodeBlock codeBlock = blockForTail.getCodeBlock();
PsiJavaToken brace = requireNonNull(codeBlock.getRBrace());
for (PsiElement element : tail) {
if (element.isValid()) {
codeBlock.addBefore(element, brace);
element.delete();
}
}
return codeBlock;
}
@NotNull
private PsiBlockStatement getBlockFromIf(PsiIfStatement ifStatement, boolean inThen) {
if (inThen) {
PsiStatement elseBranch = ifStatement.getElseBranch();
if (elseBranch == null) {
ifStatement.setElseBranch(BlockUtils.createBlockStatement(myProject));
return (PsiBlockStatement)ifStatement.getElseBranch();
}
if (!(elseBranch instanceof PsiBlockStatement)) {
return (PsiBlockStatement)BlockUtils.expandSingleStatementToBlockStatement(elseBranch).getParent().getParent();
}
return (PsiBlockStatement)elseBranch;
}
else {
PsiStatement thenBranch = ifStatement.getThenBranch();
if (thenBranch == null) {
ifStatement.setThenBranch(BlockUtils.createBlockStatement(myProject));
return (PsiBlockStatement)ifStatement.getThenBranch();
}
if (!(thenBranch instanceof PsiBlockStatement)) {
return (PsiBlockStatement)BlockUtils.expandSingleStatementToBlockStatement(thenBranch).getParent().getParent();
}
return (PsiBlockStatement)thenBranch;
}
}
private void replace() {
if (!(myReturnStatement.getParent().getParent() instanceof PsiBlockStatement)) {
myReturnStatement = BlockUtils.expandSingleStatementToBlockStatement(myReturnStatement);
}
PsiStatement[] newStatements = ContainerUtil.map2Array(
myReplacements, PsiStatement.class, text -> myFactory.createStatementFromText(text, null));
if (newStatements.length > 0) {
BlockUtils.addBefore(myReturnStatement, newStatements);
}
PsiCodeBlock block = tryCast(myReturnStatement.getParent(), PsiCodeBlock.class);
new CommentTracker().deleteAndRestoreComments(myReturnStatement);
cleanUpEmptyBlocks(block);
}
private static void cleanUpEmptyBlocks(PsiCodeBlock block) {
if (block == null || !block.isEmpty()) return;
PsiBlockStatement blockStatement = tryCast(block.getParent(), PsiBlockStatement.class);
if (blockStatement == null) return;
PsiIfStatement parent = tryCast(blockStatement.getParent(), PsiIfStatement.class);
if (parent == null) return;
PsiExpression condition = parent.getCondition();
if (condition == null) return;
if (blockStatement == parent.getElseBranch()) {
new CommentTracker().deleteAndRestoreComments(blockStatement);
}
else if (blockStatement == parent.getThenBranch()) {
if (parent.getElseBranch() != null) {
new CommentTracker().replaceAndRestoreComments(blockStatement, parent.getElseBranch());
parent.getElseBranch().delete();
CommentTracker ct = new CommentTracker();
String negatedCondition = BoolUtils.getNegatedExpressionText(condition, ct);
ct.replaceAndRestoreComments(condition, negatedCondition);
}
else if (!SideEffectChecker.mayHaveSideEffects(condition)) {
PsiCodeBlock parentBlock = tryCast(parent.getParent(), PsiCodeBlock.class);
new CommentTracker().deleteAndRestoreComments(parent);
cleanUpEmptyBlocks(parentBlock);
}
}
}
static void replaceSingleReturn(@NotNull Project project,
PsiCodeBlock block,
ExitContext exitContext,
PsiReturnStatement returnStatement) {
new ReturnReplacementContext(project, block, exitContext, returnStatement).process();
}
}
@@ -0,0 +1,7 @@
void test(String s) {
if(s != null) {
if(!s.isEmpty()) {
System.out.println(s);
}
}
}
@@ -0,0 +1,5 @@
void test(String s) {
if(s == null) return;
if(s.isEmpty()) return;
System.out.println(s);
}
@@ -0,0 +1,6 @@
<html>
<body>
<p>This intention converts the method body to single 'return' form.
</p>
</body>
</html>
@@ -0,0 +1,16 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean noEmptyStrings(String[][] list) {
boolean result = true;
for (String[] inner : list) {
for (String s : inner) {
if (s.isEmpty()) {
result = false;
break;
}
}
if (!result) break;
}
return result;
}
}
@@ -0,0 +1,13 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean hasEmptyString(List<String> list) {
boolean result = false;
for (String s : list) {
if (s.isEmpty()) {
result = true;
break;
}
}
return result;
}
}
@@ -0,0 +1,21 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean test(String[] arr) {
boolean result = false;
boolean finished = false;
if (arr != null) {
System.out.println("ok");
for (String s : arr) {
if (s.isEmpty()) {
finished = true;
break;
}
System.out.println(s);
}
if (!finished) {
result = true;
}
}
return result;
}
}
@@ -0,0 +1,27 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean test(String[] arr) {
boolean result = true;
if (arr == null) {
result = false;
} else {
String s = arr[0];
if (s == null) {
result = false;
} else {
s = arr[1];
if (s == null) {
result = false;
} else {
if (arr.length > 3) {
s = arr[2];
if (s != null && s.isEmpty()) {
result = false;
}
}
}
}
}
return result;
}
}
@@ -0,0 +1,13 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean test(String s) {
boolean result = false;
if (s != null) {
if (!s.isEmpty()) {
System.out.println(s);
result = true;
}
}
return result;
}
}
@@ -0,0 +1,21 @@
// "Transform body to single exit-point form" "true"
class Test {
String test2(List<String> list, String foo, String bar) {
String result = foo;
boolean finished = false;
for (String s : list) {
for (int i = 0; i < 10; i++) {
bar = s;
if (s.length() == i) {
finished = true;
break;
}
}
if (finished) break;
}
if (!finished) {
result = bar;
}
return result;
}
}
@@ -0,0 +1,35 @@
// "Transform body to single exit-point form" "true"
class Test {
native String get(String s);
String test(String[] data) {
String result = null;
boolean finished = false;
if (data == null) {
result = get("foo");
} else {
String s = data[0];
int i = 0;
if (data.length > 2) {
if (data[2] != null) {
if (data[2].isEmpty()) {
finished = true;
}
}
if (!finished) {
while (true) {
if (!s.isEmpty()) {
if (s.length() > 2) {
result = s;
break;
}
}
System.out.println(s);
s = data[i++];
}
}
}
}
return result;
}
}
@@ -0,0 +1,19 @@
// "Transform body to single exit-point form" "true"
class Test {
int test(String s) {
int result = 2;
if (s == null) {
if (!(Math.random() > 0.5)) {
result = 4;
}
} else {
if (s.isEmpty()) {
result = 3;
} else {
System.out.println(s);
result = 1;
}
}
return result;
}
}
@@ -0,0 +1,21 @@
// "Transform body to single exit-point form" "true"
class Test {
int test(String s) {
int result = 1;
if (s == null) {
if (Math.random() > 0.5) {
result = 2;
} else {
System.out.println("going further");
}
}
if (result == 1) {
if (s.isEmpty()) {
result = 3;
} else {
System.out.println(s);
}
}
return result;
}
}
@@ -0,0 +1,16 @@
// "Transform body to single exit-point form" "true"
class Test {
int test(String[] strings) {
int result = 0;
for (String string : strings) {
if (!string.isEmpty()) {
result = string.length();
break;// positive number
}
}
if (result == 0) {
result = strings.length;
}
return result;
}
}
@@ -0,0 +1,16 @@
// "Transform body to single exit-point form" "true"
class Test {
int test(String[] strings) {
int result = -1;
for (String string : strings) {
if (!string.equal("foo")) {
result = string.length();
break;// non-negative number
}
}
if (result == -1) {
result = strings.length;
}
return result;
}
}
@@ -0,0 +1,13 @@
// "Transform body to single exit-point form" "true"
class Test {
String test(String[] strings) {
String result = "";
if (strings.length > 2) {
String string = strings[0];
if (string.equals(strings[1])) {
result = foo(string);
}
}
return result;
}
}
@@ -0,0 +1,19 @@
// "Transform body to single exit-point form" "true"
class Test {
String test(String[] strings) {
String res = null;
boolean finished = false;
if (strings.length > 2) {
String string = strings[0];
if (string.equals(strings[1])) {
finished = true;
res = foo(string);
}
}
if (!finished) {
String result = bar();
res = result;
}
return res;
}
}
@@ -0,0 +1,18 @@
// "Transform body to single exit-point form" "true"
class Test {
String test(String[] strings) {
String result = null;
boolean finished = false;
if (strings.length > 2) {
String string = strings[0];
if (string.equals(strings[1])) {
finished = true;
result = foo(string);
}
}
if (!finished) {
result = bar();
}
return result;
}
}
@@ -0,0 +1,18 @@
// "Transform body to single exit-point form" "true"
class Test {
String test2(List<String> list, String foo, String bar) {
String result = bar;
boolean finished = false;
for (String s : list) {
for (int i = 0; i < 10; i++) {
if (s.length() == i) {
finished = true;
result = foo;
break;
}
}
if (finished) break;
}
return result;
}
}
@@ -0,0 +1,17 @@
// "Transform body to single exit-point form" "true"
class Test {
String process(String s, int x) {
String result = null;
if (x > 0) {
if (x == 2) {
result = s.trim();
} else {
System.out.println(s.substring(0));
}
}
if (result == null) {
result = s.substring(1);
}
return result;
}
}
@@ -0,0 +1,16 @@
// "Transform body to single exit-point form" "true"
class Test {
String process(String s, int x) {
String result;
if (x > 0) {
if (x == 2) {
result = s.trim();
} else {
result = s.substring(0);
}
} else {
result = s.substring(1);
}
return result;
}
}
@@ -0,0 +1,15 @@
// "Transform body to single exit-point form" "true"
class Test {
String process(String s) {
String res = null;
if (s != null) {
s = s.trim();
if (!s.isEmpty()) {
System.out.println(s);
String result = s + s;
res = result;
}
}
return res;
}
}
@@ -0,0 +1,15 @@
// "Transform body to single exit-point form" "true"
class Test {
String process(String s) {
String res = null;
if (s != null) {
s = s.trim();
if (!s.isEmpty()) {
System.out.println(s);
String result = s + s;
res = result;
}
}
return res;
}
}
@@ -0,0 +1,11 @@
// "Transform body to single exit-point form" "true"
class Test {
void test2(String[] arr) {
for (String s : arr) {
if (s.isEmpty()) {
System.out.println(s);
break;
}
}
}
}
@@ -0,0 +1,16 @@
// "Transform body to single exit-point form" "true"
class Test {
void test2(String[] arr) {
boolean finished = false;
for (String s : arr) {
if (s.isEmpty()) {
System.out.println(s);
finished = true;
break;
}
}
if (!finished) {
System.out.println("Not found");
}
}
}
@@ -0,0 +1,10 @@
// "Transform body to single exit-point form" "true"
class Test {
void test2(String s) {
if (s != null) {
if (!s.isEmpty()) {
System.out.println(s);
}
}
}
}
@@ -0,0 +1,13 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean <caret>noEmptyStrings(String[][] list) {
for (String[] inner : list) {
for (String s : inner) {
if (s.isEmpty()) {
return false;
}
}
}
return true;
}
}
@@ -0,0 +1,9 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean <caret>hasEmptyString(List<String> list) {
for (String s : list) {
if(s.isEmpty()) return true;
}
return false;
}
}
@@ -0,0 +1,14 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean <caret>test(String[] arr) {
if (arr != null) {
System.out.println("ok");
for(String s : arr) {
if (s.isEmpty()) return false;
System.out.println(s);
}
return true;
}
return false;
}
}
@@ -0,0 +1,15 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean <caret>test(String[] arr) {
if (arr == null) return false;
String s = arr[0];
if (s == null) return false;
s = arr[1];
if (s == null) return false;
if (arr.length > 3) {
s = arr[2];
if (s != null && s.isEmpty()) return false;
}
return true;
}
}
@@ -0,0 +1,9 @@
// "Transform body to single exit-point form" "true"
class Test {
boolean <caret>test(String s) {
if(s == null) return false;
if(s.isEmpty()) return false;
System.out.println(s);
return true;
}
}
@@ -0,0 +1,11 @@
// "Transform body to single exit-point form" "true"
class Test {
String <caret>test2(List<String> list, String foo, String bar) {
for(String s : list)
for(int i=0; i<10; i++) {
bar = s;
if(s.length() == i) return foo;
}
return bar;
}
}
@@ -0,0 +1,29 @@
// "Transform body to single exit-point form" "true"
class Test {
native String get(String s);
String <caret>test(String[] data) {
if (data == null) {
return get("foo");
}
String s = data[0];
int i=0;
if (data.length > 2) {
if (data[2] != null) {
if(data[2].isEmpty()) {
return null;
}
}
while (true) {
if (!s.isEmpty()) {
if (s.length() > 2) {
return s;
}
}
System.out.println(s);
s = data[i++];
}
}
return null;
}
}
@@ -0,0 +1,12 @@
// "Transform body to single exit-point form" "true"
class Test {
int <caret>test(String s) {
if(s == null) {
if (Math.random() > 0.5) return 2;
return 4;
}
if(s.isEmpty()) return 3;
System.out.println(s);
return 1;
}
}
@@ -0,0 +1,12 @@
// "Transform body to single exit-point form" "true"
class Test {
int <caret>test(String s) {
if(s == null) {
if (Math.random() > 0.5) return 2;
System.out.println("going further");
}
if(s.isEmpty()) return 3;
System.out.println(s);
return 1;
}
}
@@ -0,0 +1,11 @@
// "Transform body to single exit-point form" "true"
class Test {
int <caret>test(String[] strings) {
for (String string : strings) {
if (!string.isEmpty()) {
return string.length(); // positive number
}
}
return strings.length;
}
}
@@ -0,0 +1,11 @@
// "Transform body to single exit-point form" "true"
class Test {
int <caret>test(String[] strings) {
for (String string : strings) {
if (!string.equal("foo")) {
return string.length(); // non-negative number
}
}
return strings.length;
}
}
@@ -0,0 +1,12 @@
// "Transform body to single exit-point form" "true"
class Test {
String <caret>test(String[] strings) {
if (strings.length > 2) {
String string = strings[0];
if (string.equals(strings[1])) {
return foo(string);
}
}
return "";
}
}
@@ -0,0 +1,13 @@
// "Transform body to single exit-point form" "true"
class Test {
String <caret>test(String[] strings) {
if (strings.length > 2) {
String string = strings[0];
if (string.equals(strings[1])) {
return foo(string);
}
}
String result = bar();
return result;
}
}
@@ -0,0 +1,12 @@
// "Transform body to single exit-point form" "true"
class Test {
String <caret>test(String[] strings) {
if (strings.length > 2) {
String string = strings[0];
if (string.equals(strings[1])) {
return foo(string);
}
}
return bar();
}
}
@@ -0,0 +1,10 @@
// "Transform body to single exit-point form" "true"
class Test {
String <caret>test2(List<String> list, String foo, String bar) {
for(String s : list)
for(int i=0; i<10; i++) {
if(s.length() == i) return foo;
}
return bar;
}
}
@@ -0,0 +1,12 @@
// "Transform body to single exit-point form" "true"
class Test {
String<caret> process(String s, int x) {
if (x > 0) {
if (x == 2) {
return s.trim();
}
System.out.println(s.substring(0));
}
return s.substring(1);
}
}
@@ -0,0 +1,12 @@
// "Transform body to single exit-point form" "true"
class Test {
String<caret> process(String s, int x) {
if (x > 0) {
if (x == 2) {
return s.trim();
}
return s.substring(0);
}
return s.substring(1);
}
}
@@ -0,0 +1,11 @@
// "Transform body to single exit-point form" "true"
class Test {
String<caret> process(String s) {
if (s == null) return null;
s = s.trim();
if (s.isEmpty()) return null;
System.out.println(s);
String result = s + s;
return result;
}
}
@@ -0,0 +1,15 @@
// "Transform body to single exit-point form" "true"
class Test {
String<caret> process(String s) {
if (s == null) {
return null;
}
s = s.trim();
if (s.isEmpty()) {
return null;
}
System.out.println(s);
String result = s + s;
return result;
}
}
@@ -0,0 +1,11 @@
// "Transform body to single exit-point form" "true"
class Test {
void <caret>test2(String[] arr) {
for(String s : arr) {
if (s.isEmpty()) {
System.out.println(s);
return;
}
}
}
}
@@ -0,0 +1,12 @@
// "Transform body to single exit-point form" "true"
class Test {
void <caret>test2(String[] arr) {
for(String s : arr) {
if (s.isEmpty()) {
System.out.println(s);
return;
}
}
System.out.println("Not found");
}
}
@@ -0,0 +1,9 @@
// "Transform body to single exit-point form" "true"
class Test {
void <caret>test2(String s) {
if(s == null) return;
if(s.isEmpty()) return;
System.out.println(s);
return;
}
}
@@ -0,0 +1,11 @@
// Copyright 2000-2019 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.java.codeInsight.intention;
import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
public class ConvertToSingleReturnActionTest extends LightIntentionActionTestCase {
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/convertToSingleReturn";
}
}
@@ -574,4 +574,6 @@ collapse.selection.overlapping.warning.text=Overlapping fold region(s) exist
collapse.selection.overlapping.warning.ok=Remove
collapse.selection.overlapping.warning.cancel=Cancel
change.uid.action.name=Randomly change 'serialVersionUID' initializer
change.uid.action.name=Randomly change 'serialVersionUID' initializer
intention.convert.to.single.return.name=Transform body to single exit-point form