PY-16055 Add quickfix and tests for functions with incorrect return annotation

Add quickfix for functions whose return type doesn't match their annotated type.
This commit is contained in:
Lada Gagina
2016-08-30 17:12:55 +03:00
parent dd95592ff4
commit c9a96f59ff
10 changed files with 143 additions and 4 deletions
@@ -23,6 +23,7 @@ import com.intellij.util.ArrayFactory;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.psi.stubs.PyFunctionStub;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -49,6 +50,9 @@ public interface PyFunction extends PsiNamedElement, StubBasedPsiElement<PyFunct
@Nullable
ASTNode getNameNode();
@Nullable
PyType getReturnStatementType(TypeEvalContext typeEvalContext);
@Nullable
PyType getReturnTypeFromDocString();
@@ -125,6 +125,9 @@ QFIX.NAME.make.static=Make method static
#PyMakeFunctionFromMethodQuickFix
QFIX.NAME.make.function=Make function from method
#PyMakeFunctionReturnTypeQuickFix
QFIX.NAME.make.$0.return.$1=Make ''{0}'' return ''{1}''
#PyImplementMethodsQuickFix
QFIX.NAME.implement.methods=Implement abstract methods
@@ -24,12 +24,12 @@ import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.hash.LinkedHashMap;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.documentation.PythonDocumentationProvider;
import com.jetbrains.python.inspections.quickfix.PyMakeFunctionReturnTypeQuickFix;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.types.*;
import org.jetbrains.annotations.Nls;
@@ -103,8 +103,11 @@ public class PyTypeCheckerInspection extends PyInspection {
if (!PyTypeChecker.match(expected, actual, myTypeEvalContext)) {
final String expectedName = PythonDocumentationProvider.getTypeName(expected, myTypeEvalContext);
final String actualName = PythonDocumentationProvider.getTypeName(actual, myTypeEvalContext);
PyMakeFunctionReturnTypeQuickFix localQuickFix = new PyMakeFunctionReturnTypeQuickFix(function, actualName, myTypeEvalContext);
PyMakeFunctionReturnTypeQuickFix globalQuickFix = new PyMakeFunctionReturnTypeQuickFix(function, null, myTypeEvalContext);
registerProblem(returnExpr != null ? returnExpr : node,
String.format("Expected type '%s', got '%s' instead", expectedName, actualName));
String.format("Expected type '%s', got '%s' instead", expectedName, actualName),
localQuickFix, globalQuickFix);
}
}
}
@@ -141,7 +144,7 @@ public class PyTypeCheckerInspection extends PyInspection {
@Override
public void visitPyReturnStatement(PyReturnStatement node) {
if (PsiTreeUtil.getParentOfType(node, ScopeOwner.class, true) == myFunction) {
if (ScopeUtil.getScopeOwner(node) == myFunction) {
myHasReturns = true;
}
}
@@ -0,0 +1,83 @@
/*
* Copyright 2000-2016 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.quickfix;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiComment;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.documentation.PythonDocumentationProvider;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author lada
*/
public class PyMakeFunctionReturnTypeQuickFix implements LocalQuickFix {
private final SmartPsiElementPointer<PyFunction> myFunction;
private final SmartPsiElementPointer<PyAnnotation> myAnnotation;
private final SmartPsiElementPointer<PsiComment> myTypeCommentAnnotation;
private final String myReturnTypeName;
public PyMakeFunctionReturnTypeQuickFix(@NotNull PyFunction function, @Nullable String returnTypeName, @NotNull TypeEvalContext context) {
final SmartPointerManager manager = SmartPointerManager.getInstance(function.getProject());
myFunction = manager.createSmartPsiElementPointer(function);
PyAnnotation annotation = function.getAnnotation();
myAnnotation = annotation != null ? manager.createSmartPsiElementPointer(annotation) : null;
PsiComment typeCommentAnnotation = function.getTypeComment();
myTypeCommentAnnotation = typeCommentAnnotation != null ? manager.createSmartPsiElementPointer(typeCommentAnnotation) : null;
myReturnTypeName = (returnTypeName == null) ? PythonDocumentationProvider.getTypeName(function.getReturnStatementType(context), context) : returnTypeName;
}
@NotNull
public String getName() {
PyFunction function = myFunction.getElement();
String functionName = function != null ? function.getName() : "function";
return PyBundle.message("QFIX.NAME.make.$0.return.$1", functionName, myReturnTypeName);
}
@NotNull
public String getFamilyName() {
return PyBundle.message("QFIX.NAME.make.$0.return.$1", "function", "inferred type");
}
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project);
if (myAnnotation != null) {
final PyAnnotation annotation = myAnnotation.getElement();
if (annotation != null) {
final PyExpression annotationExpr = annotation.getValue();
if (annotationExpr == null) return;
annotationExpr.replace(elementGenerator.createExpressionFromText(LanguageLevel.PYTHON30, myReturnTypeName));
}
}
else if (myTypeCommentAnnotation != null) {
final PsiComment typeComment = myTypeCommentAnnotation.getElement();
if (typeComment != null) {
final StringBuilder typeCommentAnnotation = new StringBuilder(typeComment.getText());
typeCommentAnnotation.delete(typeCommentAnnotation.indexOf("->"), typeCommentAnnotation.length());
typeCommentAnnotation.append("-> ").append(myReturnTypeName);
final PsiComment newTypeComment = elementGenerator.createFromText(LanguageLevel.PYTHON27, PsiComment.class, typeCommentAnnotation.toString());
typeComment.replace(newTypeComment);
}
}
}
}
@@ -517,7 +517,7 @@ public class PyFunctionImpl extends PyBaseElementImpl<PyFunctionStub> implements
@Override
public void visitPyReturnStatement(PyReturnStatement node) {
if (PsiTreeUtil.getParentOfType(node, ScopeOwner.class, true) == myFunction) {
if (ScopeUtil.getScopeOwner(node) == myFunction) {
final PyExpression expr = node.getExpression();
PyType returnType;
returnType = expr == null ? PyNoneType.INSTANCE : myContext.getType(expr);
@@ -0,0 +1,3 @@
def f(x):
# type: (int) -> str
return <warning descr="Expected type 'str', got 'int' instead">42<caret></warning>
@@ -0,0 +1,3 @@
def f(x):
# type: (int) -> int
return 42
@@ -0,0 +1,2 @@
def f(x) -> str:
return <warning descr="Expected type 'str', got 'int' instead">42<caret></warning>
@@ -0,0 +1,2 @@
def f(x) -> int:
return 42
@@ -0,0 +1,36 @@
/*
* Copyright 2000-2016 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.quickFixes;
import com.intellij.testFramework.TestDataPath;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyQuickFixTestCase;
import com.jetbrains.python.inspections.PyTypeCheckerInspection;
import com.jetbrains.python.psi.LanguageLevel;
/**
* @author lada
*/
@TestDataPath("$CONTENT_ROOT/../testData/quickFixes/PyMakeFunctionReturnTypeQuickFixTest/")
public class PyMakeFunctionReturnTypeQuickFixTest extends PyQuickFixTestCase {
public void testOneReturn() {
doQuickFixTest(PyTypeCheckerInspection.class, PyBundle.message("QFIX.NAME.make.$0.return.$1", "f", "int"), LanguageLevel.PYTHON27);
}
public void testPy3OneReturn() {
doQuickFixTest(PyTypeCheckerInspection.class, PyBundle.message("QFIX.NAME.make.$0.return.$1", "f", "int"), LanguageLevel.PYTHON30);
}
}