blob: 47ed78e48760d767e05e8e05e949d59cf637c132 [file] [log] [blame]
/*
* 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.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.controlFlow.ControlFlowUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.util.RefactoringUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ConcurrentWeakHashMap;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class VariableAccessFromInnerClassFix implements IntentionAction {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.quickfix.VariableAccessFromInnerClassFix");
private final PsiVariable myVariable;
private final PsiElement myContext;
private final int myFixType;
private static final int MAKE_FINAL = 0;
private static final int MAKE_ARRAY = 1;
private static final int COPY_TO_FINAL = 2;
private static final Key<Map<PsiVariable,Boolean>>[] VARS = new Key[] {Key.create("VARS_TO_MAKE_FINAL"), Key.create("VARS_TO_TRANSFORM"), Key.create("???")};
public VariableAccessFromInnerClassFix(@NotNull PsiVariable variable, @NotNull PsiElement element) {
myVariable = variable;
myContext = element;
myFixType = getQuickFixType(variable);
if (myFixType == -1) return;
getVariablesToFix().add(variable);
}
@Override
@NotNull
public String getText() {
@NonNls String message;
switch (myFixType) {
case MAKE_FINAL:
message = "make.final.text";
break;
case MAKE_ARRAY:
message = "make.final.transform.to.one.element.array";
break;
case COPY_TO_FINAL:
return QuickFixBundle.message("make.final.copy.to.temp", myVariable.getName());
default:
return "";
}
Collection<PsiVariable> vars = getVariablesToFix();
String varNames = vars.size() == 1 ? "'"+myVariable.getName()+"'" : "variables";
return QuickFixBundle.message(message, varNames);
}
@Override
@NotNull
public String getFamilyName() {
return QuickFixBundle.message("make.final.family");
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return myContext.isValid() &&
myContext.getManager().isInProject(myContext) &&
myVariable.isValid() &&
myFixType != -1 &&
!getVariablesToFix().isEmpty() &&
!inOwnInitializer(myVariable, myContext);
}
private static boolean inOwnInitializer(PsiVariable variable, PsiElement context) {
return PsiTreeUtil.isAncestor(variable, context, false);
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) {
if (!FileModificationService.getInstance().preparePsiElementsForWrite(myContext, myVariable)) return;
try {
switch (myFixType) {
case MAKE_FINAL:
makeFinal();
break;
case MAKE_ARRAY:
makeArray();
break;
case COPY_TO_FINAL:
copyToFinal();
break;
}
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
finally {
getVariablesToFix().clear();
}
}
private void makeArray() {
for (PsiVariable var : getVariablesToFix()) {
makeArray(var);
}
}
@NotNull
private Collection<PsiVariable> getVariablesToFix() {
Map<PsiVariable, Boolean> vars = myContext.getUserData(VARS[myFixType]);
if (vars == null) myContext.putUserData(VARS[myFixType], vars = new ConcurrentWeakHashMap<PsiVariable, Boolean>(1));
final Map<PsiVariable, Boolean> finalVars = vars;
return new AbstractCollection<PsiVariable>() {
@Override
public boolean add(PsiVariable psiVariable) {
return finalVars.put(psiVariable, Boolean.TRUE) == null;
}
@NotNull
@Override
public Iterator<PsiVariable> iterator() {
return finalVars.keySet().iterator();
}
@Override
public int size() {
return finalVars.size();
}
};
}
private void makeFinal() {
for (PsiVariable var : getVariablesToFix()) {
if (var.isValid()) {
PsiUtil.setModifierProperty(var, PsiModifier.FINAL, true);
}
}
}
private void makeArray(PsiVariable variable) throws IncorrectOperationException {
PsiType type = variable.getType();
PsiElementFactory factory = JavaPsiFacade.getInstance(myContext.getProject()).getElementFactory();
PsiType newType = type.createArrayType();
PsiDeclarationStatement variableDeclarationStatement;
PsiExpression initializer = variable.getInitializer();
if (initializer == null) {
String expression = "[1]";
while (type instanceof PsiArrayType) {
expression += "[1]";
type = ((PsiArrayType) type).getComponentType();
}
PsiExpression init = factory.createExpressionFromText("new " + type.getCanonicalText() + expression, variable);
variableDeclarationStatement = factory.createVariableDeclarationStatement(variable.getName(), newType, init);
}
else {
PsiExpression init = factory.createExpressionFromText("{ " + initializer.getText() + " }", variable);
variableDeclarationStatement = factory.createVariableDeclarationStatement(variable.getName(), newType, init);
}
PsiVariable newVariable = (PsiVariable)variableDeclarationStatement.getDeclaredElements()[0];
PsiUtil.setModifierProperty(newVariable, PsiModifier.FINAL, true);
PsiElement newExpression = factory.createExpressionFromText(variable.getName() + "[0]", variable);
PsiElement outerCodeBlock = PsiUtil.getVariableCodeBlock(variable, null);
if (outerCodeBlock == null) return;
List<PsiReferenceExpression> outerReferences = new ArrayList<PsiReferenceExpression>();
collectReferences(outerCodeBlock, variable, outerReferences);
replaceReferences(outerReferences, newExpression);
variable.replace(newVariable);
}
private void copyToFinal() throws IncorrectOperationException {
PsiManager psiManager = myContext.getManager();
PsiElementFactory factory = JavaPsiFacade.getInstance(psiManager.getProject()).getElementFactory();
PsiExpression initializer = factory.createExpressionFromText(myVariable.getName(), myContext);
String newName = suggestNewName(psiManager.getProject(), myVariable);
PsiType type = myVariable.getType();
PsiDeclarationStatement copyDecl = factory.createVariableDeclarationStatement(newName, type, initializer);
PsiVariable newVariable = (PsiVariable)copyDecl.getDeclaredElements()[0];
PsiUtil.setModifierProperty(newVariable, PsiModifier.FINAL, true);
PsiElement statement = getStatementToInsertBefore();
if (statement == null) return;
PsiExpression newExpression = factory.createExpressionFromText(newName, myVariable);
replaceReferences(myContext, myVariable, newExpression);
if (RefactoringUtil.isLoopOrIf(statement.getParent())) {
RefactoringUtil.putStatementInLoopBody(copyDecl, statement.getParent(), statement);
} else {
statement.getParent().addBefore(copyDecl, statement);
}
}
private PsiElement getStatementToInsertBefore() {
PsiElement declarationScope = myVariable instanceof PsiParameter
? ((PsiParameter)myVariable).getDeclarationScope() : PsiUtil.getVariableCodeBlock(myVariable, null);
if (declarationScope == null) return null;
PsiElement statement = myContext;
nextInnerClass:
do {
statement = RefactoringUtil.getParentStatement(statement, false);
if (statement == null || statement.getParent() == null) {
return null;
}
PsiElement element = statement;
while (element != declarationScope && !(element instanceof PsiFile)) {
if (element instanceof PsiClass) {
statement = statement.getParent();
continue nextInnerClass;
}
element = element.getParent();
}
return statement;
}
while (true);
}
private static String suggestNewName(Project project, PsiVariable variable) {
// new name should not conflict with another variable at the variable declaration level and usage level
String name = variable.getName();
// trim last digit to suggest variable names like i1,i2, i3...
if (name.length() > 1 && Character.isDigit(name.charAt(name.length()-1))) {
name = name.substring(0,name.length()-1);
}
name = "final" + StringUtil.capitalize(StringUtil.trimStart(name, "final"));
return JavaCodeStyleManager.getInstance(project).suggestUniqueVariableName(name, variable, true);
}
private static void replaceReferences(PsiElement context, final PsiVariable variable, final PsiElement newExpression) {
context.accept(new JavaRecursiveElementVisitor() {
@Override public void visitReferenceExpression(PsiReferenceExpression expression) {
if (expression.resolve() == variable)
try {
expression.replace(newExpression);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
super.visitReferenceExpression(expression);
}
});
}
private static void replaceReferences(List<PsiReferenceExpression> references, PsiElement newExpression) throws IncorrectOperationException {
for (PsiReferenceExpression reference : references) {
reference.replace(newExpression);
}
}
private static void collectReferences(PsiElement context, final PsiVariable variable, final List<PsiReferenceExpression> references) {
context.accept(new JavaRecursiveElementWalkingVisitor() {
@Override public void visitReferenceExpression(PsiReferenceExpression expression) {
if (expression.resolve() == variable) references.add(expression);
super.visitReferenceExpression(expression);
}
});
}
private static int getQuickFixType(PsiVariable variable) {
PsiElement outerCodeBlock = PsiUtil.getVariableCodeBlock(variable, null);
if (outerCodeBlock == null) return -1;
List<PsiReferenceExpression> outerReferences = new ArrayList<PsiReferenceExpression>();
collectReferences(outerCodeBlock, variable, outerReferences);
int type = MAKE_FINAL;
for (PsiReferenceExpression expression : outerReferences) {
// if it happens that variable referenced from another inner class, make sure it can be make final from there
PsiElement innerScope = HighlightControlFlowUtil.getInnerClassVariableReferencedFrom(variable, expression);
if (innerScope != null) {
int thisType = MAKE_FINAL;
if (writtenInside(variable, innerScope)) {
// cannot make parameter array
if (variable instanceof PsiParameter) return -1;
thisType = MAKE_ARRAY;
}
if (thisType == MAKE_FINAL
&& !canBeFinal(variable, outerReferences)) {
thisType = COPY_TO_FINAL;
}
type = Math.max(type, thisType);
}
}
return type;
}
private static boolean canBeFinal(PsiVariable variable, List<PsiReferenceExpression> references) {
// if there is at least one assignment to this variable, it cannot be final
Map<PsiElement, Collection<PsiReferenceExpression>> uninitializedVarProblems = new THashMap<PsiElement, Collection<PsiReferenceExpression>>();
Map<PsiElement, Collection<ControlFlowUtil.VariableInfo>> finalVarProblems = new THashMap<PsiElement, Collection<ControlFlowUtil.VariableInfo>>();
for (PsiReferenceExpression expression : references) {
if (ControlFlowUtil.isVariableAssignedInLoop(expression, variable)) return false;
HighlightInfo highlightInfo = HighlightControlFlowUtil.checkVariableInitializedBeforeUsage(expression, variable, uninitializedVarProblems,
variable.getContainingFile());
if (highlightInfo != null) return false;
highlightInfo = HighlightControlFlowUtil.checkFinalVariableMightAlreadyHaveBeenAssignedTo(variable, expression, finalVarProblems);
if (highlightInfo != null) return false;
if (variable instanceof PsiParameter && PsiUtil.isAccessedForWriting(expression)) return false;
}
return true;
}
private static boolean writtenInside(PsiVariable variable, PsiElement element) {
if (element instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assignmentExpression = (PsiAssignmentExpression)element;
PsiExpression lExpression = assignmentExpression.getLExpression();
if (lExpression instanceof PsiReferenceExpression
&& ((PsiReferenceExpression) lExpression).resolve() == variable)
return true;
}
else if (PsiUtil.isIncrementDecrementOperation(element)) {
PsiElement operand = element instanceof PsiPostfixExpression ?
((PsiPostfixExpression) element).getOperand() :
((PsiPrefixExpression) element).getOperand();
if (operand instanceof PsiReferenceExpression
&& ((PsiReferenceExpression) operand).resolve() == variable)
return true;
}
PsiElement[] children = element.getChildren();
for (PsiElement child : children) {
if (writtenInside(variable, child)) return true;
}
return false;
}
@Override
public boolean startInWriteAction() {
return true;
}
}