diff --git a/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java index 38baa75cd223..daf3ba0e18b8 100644 --- a/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java @@ -24,6 +24,7 @@ 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; @@ -90,26 +91,62 @@ public class PyTypeCheckerInspection extends PyInspection { @Override public void visitPyReturnStatement(PyReturnStatement node) { - final PyExpression returnExpr = node.getExpression(); - if (returnExpr != null) { - ScopeOwner owner = ScopeUtil.getScopeOwner(returnExpr); - if (owner instanceof PyFunction) { - final PyFunction function = (PyFunction)owner; - final PyAnnotation annotation = function.getAnnotation(); - final String typeCommentAnnotation = function.getTypeCommentAnnotation(); - if (annotation != null || typeCommentAnnotation != null) { - final PyType actual = myTypeEvalContext.getType(returnExpr); - final PyType expected = myTypeEvalContext.getReturnType(function); - if (!PyTypeChecker.match(expected, actual, myTypeEvalContext)) { - final String expectedName = PythonDocumentationProvider.getTypeName(expected, myTypeEvalContext); - final String actualName = PythonDocumentationProvider.getTypeName(actual, myTypeEvalContext); - registerProblem(returnExpr, String.format("Expected type '%s', got '%s' instead", expectedName, actualName)); + final ScopeOwner owner = ScopeUtil.getScopeOwner(node); + if (owner instanceof PyFunction) { + final PyFunction function = (PyFunction)owner; + final PyAnnotation annotation = function.getAnnotation(); + final String typeCommentAnnotation = function.getTypeCommentAnnotation(); + if (annotation != null || typeCommentAnnotation != null) { + final PyExpression returnExpr = node.getExpression(); + final PyType actual = returnExpr != null ? myTypeEvalContext.getType(returnExpr) : PyNoneType.INSTANCE; + final PyType expected = myTypeEvalContext.getReturnType(function); + if (!PyTypeChecker.match(expected, actual, myTypeEvalContext)) { + final String expectedName = PythonDocumentationProvider.getTypeName(expected, myTypeEvalContext); + final String actualName = PythonDocumentationProvider.getTypeName(actual, myTypeEvalContext); + registerProblem(returnExpr != null ? returnExpr : node, + String.format("Expected type '%s', got '%s' instead", expectedName, actualName)); + } + } + } + } + + @Override + public void visitPyFunction(PyFunction node) { + final PyAnnotation annotation = node.getAnnotation(); + final String typeCommentAnnotation = node.getTypeCommentAnnotation(); + if (annotation != null || typeCommentAnnotation != null) { + if (!PyUtil.isEmptyFunction(node)) { + final PyStatementList statements = node.getStatementList(); + ReturnVisitor visitor = new ReturnVisitor(node); + statements.accept(visitor); + if (!visitor.myHasReturns) { + final PyType expected = myTypeEvalContext.getReturnType(node); + final String expectedName = PythonDocumentationProvider.getTypeName(expected, myTypeEvalContext); + if (expected != null && !(expected instanceof PyNoneType)) { + registerProblem(annotation != null ? annotation : node.getTypeComment(), + String.format("Expected to return '%s', got no return", expectedName)); } } } } } + private static class ReturnVisitor extends PyRecursiveElementVisitor { + private final PyFunction myFunction; + private boolean myHasReturns = false; + + public ReturnVisitor(PyFunction function) { + myFunction = function; + } + + @Override + public void visitPyReturnStatement(PyReturnStatement node) { + if (PsiTreeUtil.getParentOfType(node, ScopeOwner.class, true) == myFunction) { + myHasReturns = true; + } + } + } + private void checkCallSite(@Nullable PyCallSiteExpression callSite) { final List resultsSet = PyTypeChecker.analyzeCallSite(callSite, myTypeEvalContext); final List>> problemsSet = diff --git a/python/src/com/jetbrains/python/inspections/PyUnusedLocalInspectionVisitor.java b/python/src/com/jetbrains/python/inspections/PyUnusedLocalInspectionVisitor.java index a145c998d066..acca073da2fe 100644 --- a/python/src/com/jetbrains/python/inspections/PyUnusedLocalInspectionVisitor.java +++ b/python/src/com/jetbrains/python/inspections/PyUnusedLocalInspectionVisitor.java @@ -308,7 +308,7 @@ public class PyUnusedLocalInspectionVisitor extends PyInspectionVisitor { if (func.asMethod() != null) { Boolean isEmpty = emptyFunctions.get(func); if (isEmpty == null) { - isEmpty = isEmptyFunction(func); + isEmpty = PyUtil.isEmptyFunction(func); emptyFunctions.put(func, isEmpty); } if (isEmpty && !mayBeField) { @@ -434,43 +434,4 @@ public class PyUnusedLocalInspectionVisitor extends PyInspectionVisitor { 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; - } } diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index c3f468c34dc4..d12cab4102d1 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -1834,6 +1834,45 @@ public class PyUtil { return null; } + public static boolean isEmptyFunction(@NotNull PyFunction function) { + final PyStatementList statementList = function.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; + } + /** * This helper class allows to collect various information about AST nodes composing {@link PyStringLiteralExpression}. */ diff --git a/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnType.py b/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnType.py index 4e7345d1aadc..b3ab693eb05b 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnType.py +++ b/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnType.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional, List, Union def a(x): # type: (List[int]) -> List[str] @@ -13,7 +13,7 @@ def c(): return 'abc' def d(x): - # type: (x: int) -> List[str] + # type: (int) -> List[str] return [str(x)] def e(): @@ -35,4 +35,21 @@ def g(x): if x: return 'abc' else: - return {} \ No newline at end of file + return {} + +def h(x): + # type: (Any) -> int + return + +def i(): + # type: () -> Union[int, str] + pass + +def j(x): + # type: () -> Union[int, str] + x = 42 + +def k(): + # type: () -> None + if True: + pass \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py b/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py index 1b12b90123cb..a008ae0ea8be 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py +++ b/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py @@ -28,4 +28,17 @@ def g(x) -> int: if x: return 'abc' else: - return {} \ No newline at end of file + return {} + +def h(x) -> int: + return + +def i() -> Union[int, str]: + pass + +def j(x) -> Union[int, str]: + x = 42 + +def k() -> None: + if True: + pass \ No newline at end of file