blob: 55feead595cc8b78ecef4953109263db65f96ac3 [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.jetbrains.python.inspections;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.controlflow.ControlFlowUtil;
import com.intellij.codeInsight.controlflow.Instruction;
import com.intellij.codeInspection.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Function;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.Scope;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.inspections.quickfix.AddFieldQuickFix;
import com.jetbrains.python.inspections.quickfix.PyRemoveParameterQuickFix;
import com.jetbrains.python.inspections.quickfix.PyRemoveStatementQuickFix;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyAugAssignmentStatementNavigator;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyForStatementNavigator;
import com.jetbrains.python.psi.impl.PyImportStatementNavigator;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.search.PyOverridingMethodsSearch;
import com.jetbrains.python.psi.search.PySuperMethodsSearch;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* @author oleg
*/
public class PyUnusedLocalInspectionVisitor extends PyInspectionVisitor {
private final boolean myIgnoreTupleUnpacking;
private final boolean myIgnoreLambdaParameters;
private final boolean myIgnoreRangeIterationVariables;
private final HashSet<PsiElement> myUnusedElements;
private final HashSet<PsiElement> myUsedElements;
public PyUnusedLocalInspectionVisitor(@NotNull ProblemsHolder holder,
@NotNull LocalInspectionToolSession session,
boolean ignoreTupleUnpacking,
boolean ignoreLambdaParameters,
boolean ignoreRangeIterationVariables) {
super(holder, session);
myIgnoreTupleUnpacking = ignoreTupleUnpacking;
myIgnoreLambdaParameters = ignoreLambdaParameters;
myIgnoreRangeIterationVariables = ignoreRangeIterationVariables;
myUnusedElements = new HashSet<PsiElement>();
myUsedElements = new HashSet<PsiElement>();
}
@Override
public void visitPyFunction(final PyFunction node) {
processScope(node);
}
@Override
public void visitPyLambdaExpression(final PyLambdaExpression node) {
processScope(node);
}
@Override
public void visitPyClass(PyClass node) {
processScope(node);
}
private void processScope(final ScopeOwner owner) {
if (owner.getContainingFile() instanceof PyExpressionCodeFragment || callsLocals(owner)) {
return;
}
if (!(owner instanceof PyClass)) {
collectAllWrites(owner);
}
collectUsedReads(owner);
}
private void collectAllWrites(ScopeOwner owner) {
final Instruction[] instructions = ControlFlowCache.getControlFlow(owner).getInstructions();
for (Instruction instruction : instructions) {
final PsiElement element = instruction.getElement();
if (element instanceof PyFunction && owner instanceof PyFunction) {
if (!myUsedElements.contains(element)) {
myUnusedElements.add(element);
}
}
else if (instruction instanceof ReadWriteInstruction) {
final ReadWriteInstruction readWriteInstruction = (ReadWriteInstruction)instruction;
final ReadWriteInstruction.ACCESS access = readWriteInstruction.getAccess();
if (!access.isWriteAccess()) {
continue;
}
final String name = readWriteInstruction.getName();
// Ignore empty, wildcards, global and nonlocal names
final Scope scope = ControlFlowCache.getScope(owner);
if (name == null || "_".equals(name) || scope.isGlobal(name) || scope.isNonlocal(name)) {
continue;
}
// Ignore elements out of scope
if (element == null || !PsiTreeUtil.isAncestor(owner, element, false)) {
continue;
}
// Ignore arguments of import statement
if (PyImportStatementNavigator.getImportStatementByElement(element) != null) {
continue;
}
if (!myUsedElements.contains(element)) {
myUnusedElements.add(element);
}
}
}
}
private void collectUsedReads(final ScopeOwner owner) {
final Instruction[] instructions = ControlFlowCache.getControlFlow(owner).getInstructions();
for (int i = 0; i < instructions.length; i++) {
final Instruction instruction = instructions[i];
if (instruction instanceof ReadWriteInstruction) {
final ReadWriteInstruction readWriteInstruction = (ReadWriteInstruction)instruction;
final ReadWriteInstruction.ACCESS access = readWriteInstruction.getAccess();
if (!access.isReadAccess()) {
continue;
}
final String name = readWriteInstruction.getName();
if (name == null) {
continue;
}
final PsiElement element = instruction.getElement();
// Ignore elements out of scope
if (element == null || !PsiTreeUtil.isAncestor(owner, element, false)) {
continue;
}
final int startInstruction;
if (access.isWriteAccess()) {
final PyAugAssignmentStatement augAssignmentStatement = PyAugAssignmentStatementNavigator.getStatementByTarget(element);
startInstruction = ControlFlowUtil.findInstructionNumberByElement(instructions, augAssignmentStatement);
}
else {
startInstruction = i;
}
// Check if the element is declared out of scope, mark all out of scope write accesses as used
if (element instanceof PyReferenceExpression) {
final PyReferenceExpression ref = (PyReferenceExpression)element;
final ScopeOwner declOwner = ScopeUtil.getDeclarationScopeOwner(ref, name);
if (declOwner != null && declOwner != owner) {
Collection<PsiElement> writeElements = ScopeUtil.getReadWriteElements(name, declOwner, false, true);
for (PsiElement e : writeElements) {
myUsedElements.add(e);
myUnusedElements.remove(e);
}
}
}
ControlFlowUtil.iteratePrev(startInstruction, instructions, new Function<Instruction, ControlFlowUtil.Operation>() {
public ControlFlowUtil.Operation fun(final Instruction inst) {
final PsiElement element = inst.getElement();
// Mark function as used
if (element instanceof PyFunction) {
if (name.equals(((PyFunction)element).getName())){
myUsedElements.add(element);
myUnusedElements.remove(element);
return ControlFlowUtil.Operation.CONTINUE;
}
}
// Mark write access as used
else if (inst instanceof ReadWriteInstruction) {
final ReadWriteInstruction rwInstruction = (ReadWriteInstruction)inst;
if (rwInstruction.getAccess().isWriteAccess() && name.equals(rwInstruction.getName())) {
// For elements in scope
if (element != null && PsiTreeUtil.isAncestor(owner, element, false)) {
myUsedElements.add(element);
myUnusedElements.remove(element);
}
return ControlFlowUtil.Operation.CONTINUE;
}
}
return ControlFlowUtil.Operation.NEXT;
}
});
}
}
}
static class DontPerformException extends RuntimeException {}
private static boolean callsLocals(final ScopeOwner owner) {
try {
owner.acceptChildren(new PyRecursiveElementVisitor(){
@Override
public void visitPyCallExpression(final PyCallExpression node) {
final PyExpression callee = node.getCallee();
if (callee != null && "locals".equals(callee.getName())){
throw new DontPerformException();
}
node.acceptChildren(this); // look at call expr in arguments
}
@Override
public void visitPyFunction(final PyFunction node) {
// stop here
}
});
}
catch (DontPerformException e) {
return true;
}
return false;
}
void registerProblems() {
final PyInspectionExtension[] filters = Extensions.getExtensions(PyInspectionExtension.EP_NAME);
// Register problems
final Set<PyFunction> functionsWithInheritors = new HashSet<PyFunction>();
final Map<PyFunction, Boolean> emptyFunctions = new HashMap<PyFunction, Boolean>();
for (PsiElement element : myUnusedElements) {
boolean ignoreUnused = false;
for (PyInspectionExtension filter : filters) {
if (filter.ignoreUnused(element)) {
ignoreUnused = true;
}
}
if (ignoreUnused) continue;
if (element instanceof PyFunction) {
// Local function
final PsiElement nameIdentifier = ((PyFunction)element).getNameIdentifier();
registerWarning(nameIdentifier == null ? element : nameIdentifier,
PyBundle.message("INSP.unused.locals.local.function.isnot.used",
((PyFunction)element).getName()), new PyRemoveStatementQuickFix());
}
else if (element instanceof PyClass) {
// Local class
final PyClass cls = (PyClass)element;
final PsiElement name = cls.getNameIdentifier();
registerWarning(name != null ? name : element,
PyBundle.message("INSP.unused.locals.local.class.isnot.used", cls.getName()), new PyRemoveStatementQuickFix());
}
else {
// Local variable or parameter
String name = element.getText();
if (element instanceof PyNamedParameter || element.getParent() instanceof PyNamedParameter) {
PyNamedParameter namedParameter = element instanceof PyNamedParameter
? (PyNamedParameter) element
: (PyNamedParameter) element.getParent();
name = namedParameter.getName();
// When function is inside a class, first parameter may be either self or cls which is always 'used'.
if (namedParameter.isSelf()) {
continue;
}
if (myIgnoreLambdaParameters && PsiTreeUtil.getParentOfType(element, Callable.class) instanceof PyLambdaExpression) {
continue;
}
boolean mayBeField = false;
PyClass containingClass = null;
PyParameterList paramList = PsiTreeUtil.getParentOfType(element, PyParameterList.class);
if (paramList != null && paramList.getParent() instanceof PyFunction) {
final PyFunction func = (PyFunction) paramList.getParent();
containingClass = func.getContainingClass();
if (PyNames.INIT.equals(func.getName()) && containingClass != null) {
if (!namedParameter.isKeywordContainer() && !namedParameter.isPositionalContainer()) {
mayBeField = true;
}
}
else if (ignoreUnusedParameters(func, functionsWithInheritors)) {
continue;
}
if (func.asMethod() != null) {
Boolean isEmpty = emptyFunctions.get(func);
if (isEmpty == null) {
isEmpty = isEmptyFunction(func);
emptyFunctions.put(func, isEmpty);
}
if (isEmpty && !mayBeField) {
continue;
}
}
}
boolean canRemove = !(PsiTreeUtil.getPrevSiblingOfType(element, PyParameter.class) instanceof PySingleStarParameter) ||
PsiTreeUtil.getNextSiblingOfType(element, PyParameter.class) != null;
final List<LocalQuickFix> fixes = new ArrayList<LocalQuickFix>();
if (mayBeField) {
fixes.add(new AddFieldQuickFix(name, name, containingClass.getName(), false));
}
if (canRemove) {
fixes.add(new PyRemoveParameterQuickFix());
}
registerWarning(element, PyBundle.message("INSP.unused.locals.parameter.isnot.used", name), fixes.toArray(new LocalQuickFix[fixes.size()]));
}
else {
if (myIgnoreTupleUnpacking && isTupleUnpacking(element)) {
continue;
}
final PyForStatement forStatement = PyForStatementNavigator.getPyForStatementByIterable(element);
if (forStatement != null) {
if (!myIgnoreRangeIterationVariables || !isRangeIteration(forStatement)) {
registerProblem(element, PyBundle.message("INSP.unused.locals.local.variable.isnot.used", name),
ProblemHighlightType.LIKE_UNUSED_SYMBOL, null, new ReplaceWithWildCard());
}
}
else {
registerWarning(element, PyBundle.message("INSP.unused.locals.local.variable.isnot.used", name), new PyRemoveStatementQuickFix());
}
}
}
}
}
private boolean isRangeIteration(PyForStatement forStatement) {
final PyExpression source = forStatement.getForPart().getSource();
if (!(source instanceof PyCallExpression)) {
return false;
}
PyCallExpression expr = (PyCallExpression) source;
if (expr.isCalleeText("range", "xrange")) {
final Callable callee = expr.resolveCalleeFunction(PyResolveContext.noImplicits().withTypeEvalContext(myTypeEvalContext));
if (callee != null && PyBuiltinCache.getInstance(forStatement).isBuiltin(callee)) {
return true;
}
}
return false;
}
private static boolean ignoreUnusedParameters(PyFunction func, Set<PyFunction> functionsWithInheritors) {
if (functionsWithInheritors.contains(func)) {
return true;
}
if (PySuperMethodsSearch.search(func).findFirst() != null ||
PyOverridingMethodsSearch.search(func, true).findFirst() != null) {
functionsWithInheritors.add(func);
return true;
}
return false;
}
private boolean isTupleUnpacking(PsiElement element) {
if (!(element instanceof PyTargetExpression)) {
return false;
}
// Handling of the star expressions
PsiElement parent = element.getParent();
if (parent instanceof PyStarExpression){
element = parent;
parent = element.getParent();
}
if (parent instanceof PyTupleExpression) {
// if all the items of the tuple are unused, we still highlight all of them; if some are unused, we ignore
final PyTupleExpression tuple = (PyTupleExpression)parent;
for (PyExpression expression : tuple.getElements()) {
if (expression instanceof PyStarExpression){
if (!myUnusedElements.contains(((PyStarExpression)expression).getExpression())){
return true;
}
} else if (!myUnusedElements.contains(expression)) {
return true;
}
}
}
return false;
}
private void registerWarning(@NotNull final PsiElement element, final String msg, LocalQuickFix... quickfixes) {
registerProblem(element, msg, ProblemHighlightType.LIKE_UNUSED_SYMBOL, null, quickfixes);
}
private static class ReplaceWithWildCard implements LocalQuickFix {
@NotNull
public String getName() {
return PyBundle.message("INSP.unused.locals.replace.with.wildcard");
}
public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor descriptor) {
if (!FileModificationService.getInstance().preparePsiElementForWrite(descriptor.getPsiElement())) {
return;
}
replace(descriptor.getPsiElement());
}
private void replace(final PsiElement psiElement) {
final PyFile pyFile = (PyFile) PyElementGenerator.getInstance(psiElement.getProject()).createDummyFile(LanguageLevel.getDefault(),
"for _ in tuples:\n pass"
);
final PyExpression target = ((PyForStatement)pyFile.getStatements().get(0)).getForPart().getTarget();
CommandProcessor.getInstance().executeCommand(psiElement.getProject(), new Runnable() {
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
if (target != null) {
psiElement.replace(target);
}
}
});
}
}, getName(), null);
}
@NotNull
public String getFamilyName() {
return getName();
}
}
private static boolean isEmptyFunction(@NotNull PyFunction f) {
final PyStatementList statementList = f.getStatementList();
final PyStatement[] statements = statementList.getStatements();
if (statements.length == 0) {
return true;
}
else if (statements.length == 1) {
if (isStringLiteral(statements[0]) || isPassOrRaiseOrEmptyReturn(statements[0])) {
return true;
}
}
else if (statements.length == 2) {
if (isStringLiteral(statements[0]) && (isPassOrRaiseOrEmptyReturn(statements[1]))) {
return true;
}
}
return false;
}
private static boolean isPassOrRaiseOrEmptyReturn(PyStatement stmt) {
if (stmt instanceof PyPassStatement || stmt instanceof PyRaiseStatement) {
return true;
}
if (stmt instanceof PyReturnStatement && ((PyReturnStatement)stmt).getExpression() == null) {
return true;
}
return false;
}
private static boolean isStringLiteral(PyStatement stmt) {
if (stmt instanceof PyExpressionStatement) {
final PyExpression expr = ((PyExpressionStatement)stmt).getExpression();
if (expr instanceof PyStringLiteralExpression) {
return true;
}
}
return false;
}
}