[java-intentions] IDEA-301288 Suggest to make variable effectively final moving assignment to the branch without assignment

GitOrigin-RevId: c9953f2df87f2fa35ed97be97e48cd6240ee8fae
This commit is contained in:
Tagir Valeev
2022-09-09 15:06:38 +00:00
committed by intellij-monorepo-bot
parent 600fce247f
commit 24511394c3
22 changed files with 497 additions and 1 deletions
@@ -618,4 +618,5 @@ inspection.redundant.requires.statement.message.java.base.implicitly.required='j
reassign.variable.display.name=Reassigned variable
intention.family.name.remove.repeating.call=Remove repeating call
intention.name.remove.repeating.call=Remove repeating call ''{0}()''
intention.family.name.replace.with.expression=Replace with expression
intention.family.name.replace.with.expression=Replace with expression
intention.name.make.variable.effectively.final=Make variable effectively final
@@ -617,4 +617,10 @@ public abstract class QuickFixFactory {
public abstract @NotNull IntentionAction createConvertAnonymousToInnerAction(@NotNull PsiAnonymousClass anonymousClass);
public abstract @NotNull IntentionAction createSplitSwitchBranchWithSeveralCaseValuesAction();
/**
* @param variable variable to make an effectively final
* @return a fix that refactors code to make variable effectively final when possible. Null, if it cannot create such a fix.
*/
public abstract @Nullable IntentionAction createMakeVariableEffectivelyFinalFix(@NotNull PsiVariable variable);
}
@@ -734,6 +734,7 @@ public final class HighlightControlFlowUtil {
String text = JavaErrorBundle.message("lambda.variable.must.be.final");
HighlightInfo highlightInfo = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(context).descriptionAndTooltip(text).create();
QuickFixAction.registerQuickFixAction(highlightInfo, QUICK_FIX_FACTORY.createVariableAccessFromInnerClassFix(variable, lambdaExpression));
QuickFixAction.registerQuickFixAction(highlightInfo, QUICK_FIX_FACTORY.createMakeVariableEffectivelyFinalFix(variable));
return ErrorFixExtensionPoint.registerFixes(highlightInfo, context, "lambda.variable.must.be.final");
}
}
@@ -0,0 +1,231 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.BlockUtils;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
import com.intellij.codeInsight.intention.HighPriorityAction;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.VariableAccessUtils;
import one.util.streamex.MoreCollectors;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class MakeVarEffectivelyFinalFix extends LocalQuickFixAndIntentionActionOnPsiElement implements HighPriorityAction {
private MakeVarEffectivelyFinalFix(@NotNull PsiLocalVariable variable) {
super(variable);
}
@Override
public void invoke(@NotNull Project project,
@NotNull PsiFile file,
@Nullable Editor editor,
@NotNull PsiElement startElement,
@NotNull PsiElement endElement) {
if (!(startElement instanceof PsiLocalVariable local)) return;
EffectivelyFinalFixer fixer = ContainerUtil.find(EffectivelyFinalFixer.values(), f -> f.isAvailable(local));
if (fixer == null) return;
fixer.fix(local);
}
@Override
public @NotNull String getText() {
return JavaAnalysisBundle.message("intention.name.make.variable.effectively.final");
}
@Override
public @NotNull String getFamilyName() {
return getText();
}
public static @Nullable MakeVarEffectivelyFinalFix createFix(@NotNull PsiVariable variable) {
if (!(variable instanceof PsiLocalVariable local)) return null;
if (!ContainerUtil.exists(EffectivelyFinalFixer.values(), f -> f.isAvailable(local))) return null;
return new MakeVarEffectivelyFinalFix(local);
}
enum EffectivelyFinalFixer {
MOVE_INITIALIZER_TO_IF_BRANCH {
@Override
boolean isAvailable(@NotNull PsiLocalVariable var) {
PsiExpression initializer = var.getInitializer();
if (!ExpressionUtils.isSafelyRecomputableExpression(initializer)) return false;
if (refersToNonFinalLocal(initializer)) return false;
Branched branched = extractInitMode(var);
// Do not add too many branches
return branched != null && branched.numberOfNonInitializedBranches() <= 3;
}
@Override
void fix(@NotNull PsiLocalVariable var) {
PsiExpression initializer = Objects.requireNonNull(var.getInitializer());
Branched branched = Objects.requireNonNull(extractInitMode(var));
PsiStatement statement = JavaPsiFacade.getElementFactory(var.getProject())
.createStatementFromText(var.getName() + "=" + initializer.getText() + ";", null);
branched.addInitializer(statement);
initializer.delete();
}
};
abstract boolean isAvailable(@NotNull PsiLocalVariable var);
abstract void fix(@NotNull PsiLocalVariable var);
}
private static boolean refersToNonFinalLocal(PsiExpression initializer) {
if (initializer instanceof PsiReferenceExpression ref && ref.resolve() instanceof PsiVariable refTarget) {
if (PsiUtil.isJvmLocalVariable(refTarget) && !refTarget.hasModifierProperty(PsiModifier.FINAL)) {
PsiElement block = PsiUtil.getVariableCodeBlock(refTarget, null);
return block == null || !HighlightControlFlowUtil.isEffectivelyFinal(refTarget, block, null);
}
}
return false;
}
@Nullable
private static Branched extractInitMode(@NotNull PsiLocalVariable var) {
List<PsiExpressionStatement> initializers = initializers(var);
if (initializers.isEmpty()) return null;
if (!(var.getParent() instanceof PsiDeclarationStatement decl)) return null;
if (!(decl.getParent() instanceof PsiCodeBlock block)) return null;
PsiElement commonParent = PsiTreeUtil.findCommonParent(initializers);
PsiIfStatement ifStatement = getTopLevelIfStatement(block, commonParent);
if (ifStatement == null) return null;
InitMode mode = StreamEx.of(initializers)
.map(initStatement -> InitMode.create(initStatement, ifStatement))
.collect(MoreCollectors.reducingWithZero(ExactMode.BOTTOM, ExactMode.NOT_INITIALIZED, InitMode::join));
if (!(mode instanceof Branched branched)) return null;
return branched;
}
@Nullable
private static PsiIfStatement getTopLevelIfStatement(PsiCodeBlock block, PsiElement commonParent) {
while (true) {
PsiIfStatement ifStatement = PsiTreeUtil.getNonStrictParentOfType(commonParent, PsiIfStatement.class);
if (ifStatement == null) return null;
PsiElement parent = ifStatement.getParent();
if (parent == block) return ifStatement;
commonParent = parent;
}
}
private static @NotNull List<PsiExpressionStatement> initializers(@NotNull PsiLocalVariable var) {
PsiElement block = PsiUtil.getVariableCodeBlock(var, null);
if (block == null) return List.of();
List<PsiReferenceExpression> references = VariableAccessUtils.getVariableReferences(var, block);
List<PsiExpressionStatement> initializers = new ArrayList<>();
for (PsiReferenceExpression reference : references) {
if (!PsiUtil.isAccessedForWriting(reference)) continue;
if (!(reference.getParent() instanceof PsiAssignmentExpression assign) || assign.getOperationTokenType() != JavaTokenType.EQ) {
return List.of();
}
if (!(assign.getParent() instanceof PsiExpressionStatement statement)) return List.of();
initializers.add(statement);
}
return initializers;
}
sealed interface InitMode {
@NotNull InitMode join(@NotNull InitMode nextMode);
int numberOfNonInitializedBranches();
private static InitMode fromInitializer(@NotNull PsiStatement statement, @NotNull InitMode origMode) {
if (statement.getParent() instanceof PsiCodeBlock codeBlock && codeBlock.getParent() instanceof PsiBlockStatement block) {
statement = block;
}
if (statement.getParent() instanceof PsiIfStatement ifStatement) {
if (ifStatement.getThenBranch() == statement) {
return new Branched(ifStatement, origMode, ExactMode.NOT_INITIALIZED);
} else {
return new Branched(ifStatement, ExactMode.NOT_INITIALIZED, origMode);
}
}
return ExactMode.BOTTOM;
}
static @NotNull InitMode create(@NotNull PsiExpressionStatement statement, @NotNull PsiIfStatement topStatement) {
PsiStatement current = statement;
InitMode curMode = ExactMode.INITIALIZED;
while (current != topStatement) {
curMode = fromInitializer(current, curMode);
if (curMode instanceof Branched branched) {
current = branched.ifStatement();
} else {
return ExactMode.BOTTOM;
}
}
return curMode;
}
}
enum ExactMode implements InitMode {
INITIALIZED, NOT_INITIALIZED, BOTTOM;
@Override
public @NotNull InitMode join(@NotNull InitMode nextMode) {
if (nextMode == NOT_INITIALIZED) return this;
if (this == BOTTOM || this == INITIALIZED) return BOTTOM;
return nextMode;
}
@Override
public int numberOfNonInitializedBranches() {
return this == NOT_INITIALIZED ? 1 : 0;
}
}
record Branched(@NotNull PsiIfStatement ifStatement, @NotNull InitMode thenBranch, @NotNull InitMode elseBranch) implements InitMode {
@Override
public @NotNull InitMode join(@NotNull InitMode nextMode) {
if (nextMode == ExactMode.NOT_INITIALIZED) return this;
if (!(nextMode instanceof Branched branched)) return ExactMode.BOTTOM;
if (ifStatement() != branched.ifStatement()) return ExactMode.BOTTOM;
InitMode newThen = thenBranch().join(branched.thenBranch());
InitMode newElse = elseBranch().join(branched.elseBranch());
if (newThen == ExactMode.BOTTOM || newElse == ExactMode.BOTTOM) return ExactMode.BOTTOM;
if (newThen == newElse) return newThen;
return new Branched(ifStatement(), newThen, newElse);
}
@Override
public int numberOfNonInitializedBranches() {
return thenBranch().numberOfNonInitializedBranches() + elseBranch().numberOfNonInitializedBranches();
}
void addInitializer(@NotNull PsiStatement statement) {
if (thenBranch() == ExactMode.NOT_INITIALIZED) {
PsiStatement branch = ifStatement().getThenBranch();
if (branch == null) {
ifStatement().setThenBranch(JavaPsiFacade.getElementFactory(ifStatement().getProject()).createStatementFromText("{}", null));
branch = Objects.requireNonNull(ifStatement().getThenBranch());
}
BlockUtils.addBefore(branch, statement);
} else if (thenBranch() instanceof Branched branched) {
branched.addInitializer(statement);
}
if (elseBranch() == ExactMode.NOT_INITIALIZED) {
PsiStatement branch = ifStatement().getElseBranch();
if (branch == null) {
ifStatement().setElseBranch(JavaPsiFacade.getElementFactory(ifStatement().getProject()).createStatementFromText("{}", null));
branch = Objects.requireNonNull(ifStatement().getElseBranch());
}
BlockUtils.addBefore(branch, statement);
}
else if (elseBranch() instanceof Branched branched) {
branched.addInitializer(statement);
}
}
}
}
@@ -1178,4 +1178,9 @@ public final class QuickFixFactoryImpl extends QuickFixFactory {
public @NotNull IntentionAction createSplitSwitchBranchWithSeveralCaseValuesAction() {
return new SplitSwitchBranchWithSeveralCaseValuesAction();
}
@Override
public @Nullable IntentionAction createMakeVariableEffectivelyFinalFix(@NotNull PsiVariable variable) {
return MakeVarEffectivelyFinalFix.createFix(variable);
}
}
@@ -24,6 +24,17 @@ public final class BlockUtils {
*/
public static PsiStatement addBefore(PsiStatement anchor, PsiStatement... newStatements) {
if (newStatements.length == 0) throw new IllegalArgumentException();
if (anchor instanceof PsiBlockStatement) {
PsiCodeBlock codeBlock = ((PsiBlockStatement)anchor).getCodeBlock();
PsiJavaToken brace = codeBlock.getLBrace();
if (brace != null) {
PsiElement result = brace;
for (PsiStatement statement : newStatements) {
result = codeBlock.addAfter(statement, result);
}
return (PsiStatement)result;
}
}
PsiStatement oldStatement = anchor;
PsiElement parent = oldStatement.getParent();
while (parent instanceof PsiLabeledStatement) {
@@ -0,0 +1,14 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a) {
int x;
if (a) {
x = 1;
}
else {
x = 0;
System.out.println("hello");
}
Runnable r = () -> System.out.println(x);
}
}
@@ -0,0 +1,18 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x;
if (a) {
System.out.println("hello");
x = 1;
} else {
x = 0;
if (b) {
System.out.println("hello2");
} else if (c) {
System.out.println("hello3");
}
}
Runnable r = () -> System.out.println(x);
}
}
@@ -0,0 +1,18 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x;
if (a) {
if (b) {
x = 1;
} else {
x = 2;
}
} else if (c) {
x = 3;
} else {
x = 0;
}
Runnable r = () -> System.out.println(x);
}
}
@@ -0,0 +1,18 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x;
if (a) {
if (b) {
x = 1;
} else {
x = 0;
}
} else if (c) {
x = 3;
} else {
x = 0;
}
Runnable r = () -> System.out.println(x);
}
}
@@ -0,0 +1,19 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x;
if (a) {
x = 0;
System.out.println("hello");
} else if (b) {
x = 0;
System.out.println("hello2");
} else if (c) {
x = 0;
System.out.println("hello3");
} else {
x = 1;
}
Runnable r = () -> System.out.println(x);
}
}
@@ -0,0 +1,12 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a) {
int x;
if (a) {
x = 1;
} else {
x = 0;
}
Runnable r = () -> System.out.println(x);
}
}
@@ -0,0 +1,12 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, int y) {
int x;
if (a) {
x = 1;
} else {
x = y;
}
Runnable r = () -> System.out.println(x);
}
}
@@ -0,0 +1,11 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a) {
int x = 0;
if (a) {
x = 1;
}
else System.out.println("hello");
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,15 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x = 0;
if (a) {
System.out.println("hello");
x = 1;
} else if (b) {
System.out.println("hello2");
} else if (c) {
System.out.println("hello3");
}
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,16 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x = 0;
if (a) {
if (b) {
x = 1;
} else {
x = 2;
}
} else if (c) {
x = 3;
}
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,14 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x = 0;
if (a) {
if (b) {
x = 1;
}
} else if (c) {
x = 3;
}
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,16 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, boolean b, boolean c) {
int x = 0;
if (a) {
System.out.println("hello");
} else if (b) {
System.out.println("hello2");
} else if (c) {
System.out.println("hello3");
} else {
x = 1;
}
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,10 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a) {
int x = 0;
if (a) {
x = 1;
}
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,18 @@
// "Make variable effectively final" "false"
class X {
void test(boolean a, boolean b, boolean c, boolean d) {
int x = 0;
if (a) {
System.out.println("hello");
} else if (b) {
System.out.println("hello2");
} else if (c) {
System.out.println("hello3");
} else if (d) {
System.out.println("hello4");
} else {
x = 1;
}
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,10 @@
// "Make variable effectively final" "true-preview"
class X {
void test(boolean a, int y) {
int x = y;
if (a) {
x = 1;
}
Runnable r = () -> System.out.println(<caret>x);
}
}
@@ -0,0 +1,20 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public class MakeVarEffectivelyFinalFixTest extends LightQuickFixParameterizedTestCase {
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return LightJavaCodeInsightFixtureTestCase.JAVA_17;
}
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/makeVarEffectivelyFinal";
}
}