blob: 6f1f69b01aa3aa986950037ac38cb7df6726473d [file] [log] [blame]
/*
* Copyright 2000-2014 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.codeInspection.localCanBeFinal;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.psi.*;
import com.intellij.psi.controlFlow.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.IncorrectOperationException;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
/**
* @author max
*/
public class LocalCanBeFinal extends BaseJavaBatchLocalInspectionTool {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.localCanBeFinal.LocalCanBeFinal");
public boolean REPORT_VARIABLES = true;
public boolean REPORT_PARAMETERS = true;
public boolean REPORT_CATCH_PARAMETERS = true;
public boolean REPORT_FOREACH_PARAMETERS = true;
private final LocalQuickFix myQuickFix;
@NonNls public static final String SHORT_NAME = "LocalCanBeFinal";
public LocalCanBeFinal() {
myQuickFix = new AcceptSuggested();
}
@Override
public void writeSettings(@NotNull Element node) throws WriteExternalException {
node.addContent(new Element("option").setAttribute("name", "REPORT_VARIABLES").setAttribute("value", String.valueOf(REPORT_VARIABLES)));
node.addContent(new Element("option").setAttribute("name", "REPORT_PARAMETERS").setAttribute("value", String.valueOf(REPORT_PARAMETERS)));
if (!REPORT_CATCH_PARAMETERS) {
node.addContent(new Element("option").setAttribute("name", "REPORT_CATCH_PARAMETERS").setAttribute("value", "false"));
}
if (!REPORT_FOREACH_PARAMETERS) {
node.addContent(new Element("option").setAttribute("name", "REPORT_FOREACH_PARAMETERS").setAttribute("value", "false"));
}
}
@Override
public ProblemDescriptor[] checkMethod(@NotNull PsiMethod method, @NotNull InspectionManager manager, boolean isOnTheFly) {
List<ProblemDescriptor> list = checkCodeBlock(method.getBody(), manager, isOnTheFly);
return list == null ? null : list.toArray(new ProblemDescriptor[list.size()]);
}
@Override
public ProblemDescriptor[] checkClass(@NotNull PsiClass aClass, @NotNull InspectionManager manager, boolean isOnTheFly) {
List<ProblemDescriptor> allProblems = null;
final PsiClassInitializer[] initializers = aClass.getInitializers();
for (PsiClassInitializer initializer : initializers) {
final List<ProblemDescriptor> problems = checkCodeBlock(initializer.getBody(), manager, isOnTheFly);
if (problems != null) {
if (allProblems == null) {
allProblems = new ArrayList<ProblemDescriptor>(1);
}
allProblems.addAll(problems);
}
}
return allProblems == null ? null : allProblems.toArray(new ProblemDescriptor[allProblems.size()]);
}
@Nullable
private List<ProblemDescriptor> checkCodeBlock(final PsiCodeBlock body, final InspectionManager manager, final boolean onTheFly) {
if (body == null) return null;
final ControlFlow flow;
try {
ControlFlowPolicy policy = new ControlFlowPolicy() {
@Override
public PsiVariable getUsedVariable(@NotNull PsiReferenceExpression refExpr) {
if (refExpr.isQualified()) return null;
PsiElement refElement = refExpr.resolve();
if (refElement instanceof PsiLocalVariable || refElement instanceof PsiParameter) {
if (!isVariableDeclaredInMethod((PsiVariable)refElement)) return null;
return (PsiVariable)refElement;
}
return null;
}
@Override
public boolean isParameterAccepted(@NotNull PsiParameter psiParameter) {
return isVariableDeclaredInMethod(psiParameter);
}
@Override
public boolean isLocalVariableAccepted(@NotNull PsiLocalVariable psiVariable) {
return isVariableDeclaredInMethod(psiVariable);
}
private boolean isVariableDeclaredInMethod(PsiVariable psiVariable) {
return PsiTreeUtil.getParentOfType(psiVariable, PsiClass.class) == PsiTreeUtil.getParentOfType(body, PsiClass.class);
}
};
flow = ControlFlowFactory.getInstance(body.getProject()).getControlFlow(body, policy, false);
}
catch (AnalysisCanceledException e) {
return null;
}
int start = flow.getStartOffset(body);
int end = flow.getEndOffset(body);
final Collection<PsiVariable> writtenVariables = ControlFlowUtil.getWrittenVariables(flow, start, end, false);
final List<ProblemDescriptor> problems = new ArrayList<ProblemDescriptor>();
final HashSet<PsiVariable> result = new HashSet<PsiVariable>();
body.accept(new JavaRecursiveElementWalkingVisitor() {
@Override public void visitCodeBlock(PsiCodeBlock block) {
if (block.getParent() instanceof PsiLambdaExpression && block != body) {
final List<ProblemDescriptor> descriptors = checkCodeBlock(block, manager, onTheFly);
if (descriptors != null) {
problems.addAll(descriptors);
}
return;
}
super.visitCodeBlock(block);
PsiElement anchor = block;
if (block.getParent() instanceof PsiSwitchStatement) {
anchor = block.getParent();
}
int from = flow.getStartOffset(anchor);
int end = flow.getEndOffset(anchor);
List<PsiVariable> ssa = ControlFlowUtil.getSSAVariables(flow, from, end, true);
HashSet<PsiElement> declared = getDeclaredVariables(block);
for (PsiVariable psiVariable : ssa) {
if (declared.contains(psiVariable)) {
result.add(psiVariable);
}
}
}
@Override
public void visitCatchSection(PsiCatchSection section) {
super.visitCatchSection(section);
if (!REPORT_CATCH_PARAMETERS) return;
final PsiParameter parameter = section.getParameter();
if (PsiTreeUtil.getParentOfType(parameter, PsiClass.class) != PsiTreeUtil.getParentOfType(body, PsiClass.class)) {
return;
}
final PsiCodeBlock catchBlock = section.getCatchBlock();
if (catchBlock == null) return;
final int from = flow.getStartOffset(catchBlock);
final int end = flow.getEndOffset(catchBlock);
if (!ControlFlowUtil.getWrittenVariables(flow, from, end, false).contains(parameter)) {
writtenVariables.remove(parameter);
result.add(parameter);
}
}
@Override public void visitForeachStatement(PsiForeachStatement statement) {
super.visitForeachStatement(statement);
if (!REPORT_FOREACH_PARAMETERS) return;
final PsiParameter param = statement.getIterationParameter();
if (PsiTreeUtil.getParentOfType(param, PsiClass.class) != PsiTreeUtil.getParentOfType(body, PsiClass.class)) {
return;
}
final PsiStatement body = statement.getBody();
if (body == null) return;
int from = flow.getStartOffset(body);
int end = flow.getEndOffset(body);
if (!ControlFlowUtil.getWrittenVariables(flow, from, end, false).contains(param)) {
writtenVariables.remove(param);
result.add(param);
}
}
private HashSet<PsiElement> getDeclaredVariables(PsiCodeBlock block) {
final HashSet<PsiElement> result = new HashSet<PsiElement>();
PsiElement[] children = block.getChildren();
for (PsiElement child : children) {
child.accept(new JavaElementVisitor() {
@Override public void visitReferenceExpression(PsiReferenceExpression expression) {
visitReferenceElement(expression);
}
@Override public void visitDeclarationStatement(PsiDeclarationStatement statement) {
PsiElement[] declaredElements = statement.getDeclaredElements();
for (PsiElement declaredElement : declaredElements) {
if (declaredElement instanceof PsiVariable) result.add(declaredElement);
}
}
@Override
public void visitForStatement(PsiForStatement statement) {
super.visitForStatement(statement);
final PsiStatement initialization = statement.getInitialization();
if (!(initialization instanceof PsiDeclarationStatement)) {
return;
}
final PsiDeclarationStatement declarationStatement = (PsiDeclarationStatement)initialization;
final PsiElement[] declaredElements = declarationStatement.getDeclaredElements();
for (final PsiElement declaredElement : declaredElements) {
if (declaredElement instanceof PsiVariable) {
result.add(declaredElement);
}
}
}
});
}
return result;
}
@Override public void visitReferenceExpression(PsiReferenceExpression expression) {
}
});
if (body.getParent() instanceof PsiMethod && REPORT_PARAMETERS) {
final PsiMethod method = (PsiMethod)body.getParent();
if (!(method instanceof SyntheticElement)) { // e.g. JspHolderMethod
Collections.addAll(result, method.getParameterList().getParameters());
}
}
for (Iterator<PsiVariable> iterator = result.iterator(); iterator.hasNext(); ) {
final PsiVariable variable = iterator.next();
if (shouldBeIgnored(variable)) {
iterator.remove();
continue;
}
final PsiElement parent = variable.getParent();
if (!(parent instanceof PsiDeclarationStatement)) {
continue;
}
final PsiDeclarationStatement declarationStatement = (PsiDeclarationStatement)parent;
final PsiElement[] elements = declarationStatement.getDeclaredElements();
final PsiElement grandParent = parent.getParent();
if (elements.length > 1 && grandParent instanceof PsiForStatement) {
iterator.remove(); // do not report when more than 1 variable declared in for loop
}
}
for (PsiVariable writtenVariable : writtenVariables) {
if (writtenVariable instanceof PsiParameter) {
result.remove(writtenVariable);
}
}
if (result.isEmpty()) return null;
for (PsiVariable variable : result) {
final PsiIdentifier nameIdentifier = variable.getNameIdentifier();
PsiElement problemElement = nameIdentifier != null ? nameIdentifier : variable;
if (variable instanceof PsiParameter && !(((PsiParameter)variable).getDeclarationScope() instanceof PsiForeachStatement)) {
problems.add(manager.createProblemDescriptor(problemElement,
InspectionsBundle.message("inspection.can.be.local.parameter.problem.descriptor"),
myQuickFix, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, onTheFly));
}
else {
problems.add(manager.createProblemDescriptor(problemElement,
InspectionsBundle.message("inspection.can.be.local.variable.problem.descriptor"),
myQuickFix, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, onTheFly));
}
}
return problems;
}
private boolean shouldBeIgnored(PsiVariable psiVariable) {
if (psiVariable.hasModifierProperty(PsiModifier.FINAL)) return true;
if (psiVariable instanceof PsiLocalVariable) {
return !REPORT_VARIABLES;
}
if (psiVariable instanceof PsiParameter) {
final PsiParameter parameter = (PsiParameter)psiVariable;
final PsiElement declarationScope = parameter.getDeclarationScope();
if (declarationScope instanceof PsiCatchSection) {
return !REPORT_CATCH_PARAMETERS;
}
else if (declarationScope instanceof PsiForeachStatement) {
return !REPORT_FOREACH_PARAMETERS;
}
return !REPORT_PARAMETERS;
}
return true;
}
@Override
@NotNull
public String getDisplayName() {
return InspectionsBundle.message("inspection.local.can.be.final.display.name");
}
@Override
@NotNull
public String getGroupDisplayName() {
return GroupNames.STYLE_GROUP_NAME;
}
@Override
@NotNull
public String getShortName() {
return SHORT_NAME;
}
private static class AcceptSuggested implements LocalQuickFix {
@Override
@NotNull
public String getName() {
return InspectionsBundle.message("inspection.can.be.final.accept.quickfix");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor problem) {
if (!FileModificationService.getInstance().preparePsiElementForWrite(problem.getPsiElement())) return;
PsiElement nameIdentifier = problem.getPsiElement();
if (nameIdentifier == null) return;
PsiVariable psiVariable = PsiTreeUtil.getParentOfType(nameIdentifier, PsiVariable.class, false);
if (psiVariable == null) return;
try {
psiVariable.normalizeDeclaration();
PsiUtil.setModifierProperty(psiVariable, PsiModifier.FINAL, true);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
@Override
@NotNull
public String getFamilyName() {
return getName();
}
}
@Override
public JComponent createOptionsPanel() {
final MultipleCheckboxOptionsPanel panel = new MultipleCheckboxOptionsPanel(this);
panel.addCheckbox(InspectionsBundle.message("inspection.local.can.be.final.option"), "REPORT_VARIABLES");
panel.addCheckbox(InspectionsBundle.message("inspection.local.can.be.final.option1"), "REPORT_PARAMETERS");
panel.addCheckbox(InspectionsBundle.message("inspection.local.can.be.final.option2"), "REPORT_CATCH_PARAMETERS");
panel.addCheckbox(InspectionsBundle.message("inspection.local.can.be.final.option3"), "REPORT_FOREACH_PARAMETERS");
return panel;
}
@Override
public boolean isEnabledByDefault() {
return false;
}
}