| /* |
| * 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.google.common.collect.ImmutableList; |
| import com.intellij.codeInspection.LocalInspectionToolSession; |
| import com.intellij.codeInspection.ProblemHighlightType; |
| import com.intellij.codeInspection.ProblemsHolder; |
| import com.intellij.lang.ASTNode; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiElementVisitor; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.PsiPolyVariantReference; |
| import com.intellij.psi.util.PsiElementFilter; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.psi.util.QualifiedName; |
| import com.intellij.util.Processor; |
| import com.jetbrains.python.PyBundle; |
| import com.jetbrains.python.PyNames; |
| import com.jetbrains.python.inspections.quickfix.PyUpdatePropertySignatureQuickFix; |
| import com.jetbrains.python.inspections.quickfix.RenameParameterQuickFix; |
| import com.jetbrains.python.psi.*; |
| import com.jetbrains.python.psi.impl.PyBuiltinCache; |
| import com.jetbrains.python.psi.types.PyClassType; |
| import com.jetbrains.python.psi.types.PyNoneType; |
| import com.jetbrains.python.psi.types.PyType; |
| import com.jetbrains.python.psi.types.PyTypeChecker; |
| import org.jetbrains.annotations.Nls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Checks that arguments to property() and @property and friends are ok. |
| * <br/> |
| * User: dcheryasov |
| * Date: Jun 30, 2010 2:53:05 PM |
| */ |
| public class PyPropertyDefinitionInspection extends PyInspection { |
| |
| @Nls |
| @NotNull |
| public String getDisplayName() { |
| return PyBundle.message("INSP.NAME.property.definition"); |
| } |
| |
| private static final ImmutableList<String> SUFFIXES = ImmutableList.of(PyNames.SETTER, PyNames.DELETER); |
| |
| @NotNull |
| @Override |
| public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) { |
| return new Visitor(holder, session); |
| } |
| |
| public static class Visitor extends PyInspectionVisitor { |
| |
| private LanguageLevel myLevel; |
| private List<PyClass> myStringClasses; |
| private PyFunction myOneParamFunction; |
| private PyFunction myTwoParamFunction; // arglist with two args, 'self' and 'value' |
| |
| public Visitor(final ProblemsHolder holder, LocalInspectionToolSession session) { |
| super(holder, session); |
| PsiFile psiFile = session.getFile(); |
| // save us continuous checks for level, module, stc |
| myLevel = LanguageLevel.forElement(psiFile); |
| // string classes |
| final List<PyClass> string_classes = new ArrayList<PyClass>(2); |
| final PyBuiltinCache builtins = PyBuiltinCache.getInstance(psiFile); |
| PyClass cls = builtins.getClass("str"); |
| if (cls != null) string_classes.add(cls); |
| cls = builtins.getClass("unicode"); |
| if (cls != null) string_classes.add(cls); |
| myStringClasses = string_classes; |
| // reference signatures |
| PyClass object_class = builtins.getClass("object"); |
| if (object_class != null) { |
| final PyFunction method_repr = object_class.findMethodByName("__repr__", false); |
| if (method_repr != null) myOneParamFunction = method_repr; |
| final PyFunction method_delattr = object_class.findMethodByName("__delattr__", false); |
| if (method_delattr != null) myTwoParamFunction = method_delattr; |
| } |
| } |
| |
| |
| @Override |
| public void visitPyFile(PyFile node) { |
| super.visitPyFile(node); |
| } |
| |
| @Override |
| public void visitPyClass(final PyClass node) { |
| super.visitPyClass(node); |
| // check property() and @property |
| node.scanProperties(new Processor<Property>() { |
| @Override |
| public boolean process(Property property) { |
| PyTargetExpression target = property.getDefinitionSite(); |
| if (target != null) { |
| // target = property(); args may be all funny |
| PyCallExpression call = (PyCallExpression)target.findAssignedValue(); |
| assert call != null : "Property has a null call assigned to it"; |
| final PyArgumentList arglist = call.getArgumentList(); |
| assert arglist != null : "Property call has null arglist"; |
| CallArgumentsMapping analysis = arglist.analyzeCall(getResolveContext()); |
| // we assume fget, fset, fdel, doc names |
| for (Map.Entry<PyExpression, PyNamedParameter> entry: analysis.getPlainMappedParams().entrySet()) { |
| final String param_name = entry.getValue().getName(); |
| PyExpression argument = PyUtil.peelArgument(entry.getKey()); |
| checkPropertyCallArgument(param_name, argument, node.getContainingFile()); |
| } |
| } |
| else { |
| // @property; we only check getter, others are checked by visitPyFunction |
| // getter is always present with this form |
| final Callable callable = property.getGetter().valueOrNull(); |
| if (callable instanceof PyFunction) { |
| checkGetter(callable, getFunctionMarkingElement((PyFunction)callable)); |
| } |
| } |
| return false; // always want more |
| } |
| |
| }, false); |
| } |
| |
| private void checkPropertyCallArgument(String param_name, PyExpression argument, PsiFile containingFile) { |
| assert argument != null : "Parameter mapped to null argument"; |
| Callable callable = null; |
| if (argument instanceof PyReferenceExpression) { |
| final PsiPolyVariantReference reference = ((PyReferenceExpression)argument).getReference(getResolveContext()); |
| if (reference != null) { |
| PsiElement resolved = reference.resolve(); |
| if (resolved instanceof Callable) { |
| callable = (Callable)resolved; |
| } |
| else { |
| reportNonCallableArg(resolved, argument); |
| return; |
| } |
| } |
| } |
| else if (argument instanceof PyLambdaExpression) callable = (PyLambdaExpression)argument; |
| else if (! "doc".equals(param_name)) { |
| reportNonCallableArg(argument, argument); |
| return; |
| } |
| if (callable != null && callable.getContainingFile() != containingFile) { |
| return; |
| } |
| if ("fget".equals(param_name)) checkGetter(callable, argument); |
| else if ("fset".equals(param_name)) checkSetter(callable, argument); |
| else if ("fdel".equals(param_name)) checkDeleter(callable, argument); |
| else if ("doc".equals(param_name)) { |
| PyType type = myTypeEvalContext.getType(argument); |
| if (! (type instanceof PyClassType && myStringClasses.contains(((PyClassType)type).getPyClass()))) { |
| registerProblem(argument, PyBundle.message("INSP.doc.param.should.be.str")); |
| } |
| } |
| } |
| |
| private void reportNonCallableArg(PsiElement resolved, PsiElement element) { |
| if (resolved instanceof PySubscriptionExpression || resolved instanceof PyNoneLiteralExpression) { |
| return; |
| } |
| if (PyNames.NONE.equals(element.getText())) { |
| return; |
| } |
| if (resolved instanceof PyTypedElement) { |
| final PyType type = myTypeEvalContext.getType((PyTypedElement)resolved); |
| final Boolean isCallable = PyTypeChecker.isCallable(type); |
| if (isCallable != null && !isCallable) { |
| registerProblem(element, PyBundle.message("INSP.strange.arg.want.callable")); |
| } |
| } |
| } |
| |
| @Override |
| public void visitPyFunction(PyFunction node) { |
| super.visitPyFunction(node); |
| if (myLevel.isAtLeast(LanguageLevel.PYTHON26)) { |
| // check @foo.setter and @foo.deleter |
| PyClass cls = node.getContainingClass(); |
| if (cls != null) { |
| final PyDecoratorList decos = node.getDecoratorList(); |
| if (decos != null) { |
| String name = node.getName(); |
| for (PyDecorator deco : decos.getDecorators()) { |
| final QualifiedName q_name = deco.getQualifiedName(); |
| if (q_name != null) { |
| List<String> name_parts = q_name.getComponents(); |
| if (name_parts.size() == 2) { |
| final int suffix_index = SUFFIXES.indexOf(name_parts.get(1)); |
| if (suffix_index >= 0) { |
| if (Comparing.equal(name, name_parts.get(0))) { |
| // names are ok, what about signatures? |
| PsiElement markable = getFunctionMarkingElement(node); |
| if (suffix_index == 0) checkSetter(node, markable); |
| else checkDeleter(node, markable); |
| } |
| else { |
| registerProblem(deco, PyBundle.message("INSP.func.property.name.mismatch")); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @Nullable |
| private static PsiElement getFunctionMarkingElement(PyFunction node) { |
| if (node == null) return null; |
| final ASTNode name_node = node.getNameNode(); |
| PsiElement markable = node; |
| if (name_node != null) markable = name_node.getPsi(); |
| return markable; |
| } |
| |
| |
| private void checkGetter(Callable callable, PsiElement being_checked) { |
| if (callable != null) { |
| checkOneParameter(callable, being_checked, true); |
| checkReturnValueAllowed(callable, being_checked, true, PyBundle.message("INSP.getter.return.smth")); |
| } |
| } |
| |
| private void checkSetter(Callable callable, PsiElement being_checked) { |
| if (callable != null) { |
| // signature: at least two params, more optionals ok; first arg 'self' |
| final PyParameterList param_list = callable.getParameterList(); |
| if (myTwoParamFunction != null && !PyUtil.isSignatureCompatibleTo(callable, myTwoParamFunction, myTypeEvalContext)) { |
| registerProblem(being_checked, PyBundle.message("INSP.setter.signature.advice"), new PyUpdatePropertySignatureQuickFix(true)); |
| } |
| checkForSelf(param_list); |
| // no explicit return type |
| checkReturnValueAllowed(callable, being_checked, false, PyBundle.message("INSP.setter.should.not.return")); |
| } |
| } |
| |
| private void checkDeleter(Callable callable, PsiElement being_checked) { |
| if (callable != null) { |
| checkOneParameter(callable, being_checked, false); |
| checkReturnValueAllowed(callable, being_checked, false, PyBundle.message("INSP.deleter.should.not.return")); |
| } |
| } |
| |
| private void checkOneParameter(Callable callable, PsiElement beingChecked, boolean isGetter) { |
| final PyParameterList parameterList = callable.getParameterList(); |
| if (myOneParamFunction != null && !PyUtil.isSignatureCompatibleTo(callable, myOneParamFunction, myTypeEvalContext)) { |
| if (isGetter) { |
| registerProblem(beingChecked, PyBundle.message("INSP.getter.signature.advice"), |
| new PyUpdatePropertySignatureQuickFix(false)); |
| } |
| else { |
| registerProblem(beingChecked, PyBundle.message("INSP.deleter.signature.advice"), new PyUpdatePropertySignatureQuickFix(false)); |
| } |
| } |
| checkForSelf(parameterList); |
| } |
| |
| private void checkForSelf(PyParameterList param_list) { |
| PyParameter[] parameters = param_list.getParameters(); |
| final PyClass cls = PsiTreeUtil.getParentOfType(param_list, PyClass.class); |
| if (cls != null && cls.isSubclass("type")) return; |
| if (parameters.length > 0 && ! PyNames.CANONICAL_SELF.equals(parameters[0].getName())) { |
| registerProblem( |
| parameters[0], PyBundle.message("INSP.accessor.first.param.is.$0", PyNames.CANONICAL_SELF), ProblemHighlightType.WEAK_WARNING, null, |
| new RenameParameterQuickFix(PyNames.CANONICAL_SELF)); |
| } |
| } |
| |
| private void checkReturnValueAllowed(Callable callable, PsiElement being_checked, boolean allowed, String message) { |
| // TODO: use a real flow analysis to check all exit points |
| boolean hasReturns; |
| if (callable instanceof PyFunction) { |
| final PsiElement[] returnStatements = PsiTreeUtil.collectElements(callable, new PsiElementFilter() { |
| @Override |
| public boolean isAccepted(PsiElement element) { |
| return (element instanceof PyReturnStatement && ((PyReturnStatement) element).getExpression() != null) || |
| (element instanceof PyYieldExpression); |
| } |
| }); |
| hasReturns = returnStatements.length > 0; |
| } |
| else { |
| final PyType type = myTypeEvalContext.getReturnType(callable); |
| hasReturns = !(type instanceof PyNoneType); |
| } |
| if (allowed ^ hasReturns) { |
| if (allowed && callable instanceof PyFunction) { |
| // one last chance: maybe there's no return but a 'raise' statement, see PY-4043, PY-5048 |
| PyStatementList statementList = ((PyFunction)callable).getStatementList(); |
| for (PyStatement stmt : statementList.getStatements()) { |
| if (stmt instanceof PyRaiseStatement) { |
| return; |
| } |
| } |
| } |
| registerProblem(being_checked, message); |
| } |
| } |
| } |
| |
| } |