diff --git a/python/python-ast/src/com/jetbrains/python/ast/PyAstElementVisitor.kt b/python/python-ast/src/com/jetbrains/python/ast/PyAstElementVisitor.kt index fc9be1a2ef71..0b44a5577f53 100644 --- a/python/python-ast/src/com/jetbrains/python/ast/PyAstElementVisitor.kt +++ b/python/python-ast/src/com/jetbrains/python/ast/PyAstElementVisitor.kt @@ -101,6 +101,10 @@ open class PyAstElementVisitor : PsiElementVisitor() { open fun visitPyReturnStatement(node: PyAstReturnStatement) { visitPyStatement(node) } + + open fun visitPyPassStatement(node: PyAstPassStatement) { + visitPyStatement(node) + } open fun visitPyYieldExpression(node: PyAstYieldExpression) { visitPyExpression(node) diff --git a/python/python-ast/src/com/jetbrains/python/ast/PyAstPassStatement.java b/python/python-ast/src/com/jetbrains/python/ast/PyAstPassStatement.java index f6bde68f6fce..47cc738faad6 100644 --- a/python/python-ast/src/com/jetbrains/python/ast/PyAstPassStatement.java +++ b/python/python-ast/src/com/jetbrains/python/ast/PyAstPassStatement.java @@ -6,4 +6,8 @@ import org.jetbrains.annotations.ApiStatus; @ApiStatus.Experimental public interface PyAstPassStatement extends PyAstStatement { + @Override + default void acceptPyVisitor(PyAstElementVisitor pyVisitor) { + pyVisitor.visitPyPassStatement(this); + } } diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java b/python/python-psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java index aeea9de4a220..20d34cc671cb 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java @@ -274,6 +274,10 @@ public class PyElementVisitor extends PsiElementVisitor { visitPyElement(node); } + public void visitPyPassStatement(@NotNull PyPassStatement node) { + visitPyStatement(node); + } + public void visitPyNoneLiteralExpression(@NotNull PyNoneLiteralExpression node) { visitPyElement(node); } diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/PyFunction.java b/python/python-psi-api/src/com/jetbrains/python/psi/PyFunction.java index e168159fab2c..1e6b52f93017 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/PyFunction.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/PyFunction.java @@ -8,9 +8,11 @@ import com.jetbrains.python.PyNames; import com.jetbrains.python.ast.*; import com.jetbrains.python.ast.impl.PyPsiUtilsCore; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.psi.impl.PyTypeProvider; 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.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -28,9 +30,38 @@ public interface PyFunction extends PyAstFunction, StubBasedPsiElement ARRAY_FACTORY = count -> count == 0 ? EMPTY_ARRAY : new PyFunction[count]; + /** + * Infers function's return type by analyzing only return statements (including implicit returns) in its control flow. + * Does not consider yield statements or return type annotations. + * + * @see PyFunction#getInferredReturnType(TypeEvalContext) + */ @Nullable PyType getReturnStatementType(@NotNull TypeEvalContext context); + /** + * Infers function's return type by analyzing return statements (including implicit returns) and yield expression. + * In contrast with {@link TypeEvalContext#getReturnType(PyCallable)} does not consider + * return type annotations or any other {@link PyTypeProvider}. + * + * @apiNote Does not cache the result. + */ + @ApiStatus.Internal + @Nullable + PyType getInferredReturnType(@NotNull TypeEvalContext context); + + /** + * Returns a list of all function exit points that can return a value. + * This includes explicit 'return' statements and statements that can complete + * normally with an implicit 'return None', excluding statements that raise exceptions. + * + * @see PyFunction#getReturnStatementType(TypeEvalContext) + * @return List of exit point statements, in control flow order + */ + @ApiStatus.Internal + @NotNull + List getReturnPoints(@NotNull TypeEvalContext context); + /** * Checks whether the function contains a yield expression in its body. */ diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 5a518a63d687..27c8bb468241 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -460,6 +460,9 @@ QFIX.remove.decorator=Remove decorator QFIX.NAME.make.function.return.type=Make function return inferred type QFIX.make.function.return.type=Make ''{0}'' return ''{1}'' +# PyMakeReturnsExplicitQuickFix +QFIX.NAME.make.return.stmts.explicit=Make 'return None' statements explicit + # Add method quick-fix QFIX.NAME.add.method.to.class=Add method to class QFIX.add.method.to.class=Add method {0}() to class {1} @@ -1055,7 +1058,7 @@ INSP.NAME.type.checker=Incorrect type INSP.type.checker.expected.type.got.type.instead=Expected type ''{0}'', got ''{1}'' instead INSP.type.checker.typed.dict.extra.key=Extra key ''{0}'' for TypedDict ''{1}'' INSP.type.checker.typed.dict.missing.keys=TypedDict ''{0}'' has missing {1,choice,1#key|2#keys}: {2} -INSP.type.checker.expected.to.return.type.got.no.return=Expected to return ''{0}'', got no return +INSP.type.checker.returning.type.has.implicit.return=Function returning ''{0}'' has implicit ''return None'' INSP.type.checker.init.should.return.none=__init__ should return None INSP.type.checker.type.does.not.have.expected.attribute=Type ''{0}'' doesn''t have expected {1,choice,1#attribute|2#attributes} {2} INSP.type.checker.only.concrete.class.can.be.used.where.matched.protocol.expected=Only a concrete class can be used where ''{0}'' (matched generic type ''{1}'') protocol is expected diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyStringFormatInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyStringFormatInspection.java index a90491f5ffd1..155dd94ef538 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyStringFormatInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyStringFormatInspection.java @@ -628,7 +628,7 @@ public final class PyStringFormatInspection extends PyInspection { allForSure = allForSure && elementsCount != -1; maxNumber = Math.max(maxNumber, elementsCount); } - else { + else if (!(member instanceof PyNoneType)) { allForSure = false; } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java index 275dcc64faa6..27ec4e90d1d1 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java @@ -18,6 +18,7 @@ import com.jetbrains.python.codeInsight.typing.PyProtocolsKt; import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.documentation.PythonDocumentationProvider; import com.jetbrains.python.inspections.quickfix.PyMakeFunctionReturnTypeQuickFix; +import com.jetbrains.python.inspections.quickfix.PyMakeReturnsExplicitFix; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.types.*; @@ -27,6 +28,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import static com.intellij.util.containers.ContainerUtil.exists; import static com.jetbrains.python.psi.PyUtil.as; import static com.jetbrains.python.psi.impl.PyCallExpressionHelper.*; @@ -44,10 +46,17 @@ public class PyTypeCheckerInspection extends PyInspection { } public static class Visitor extends PyInspectionVisitor { - public Visitor(@Nullable ProblemsHolder holder, @NotNull TypeEvalContext context) { + public Visitor(@NotNull ProblemsHolder holder, @NotNull TypeEvalContext context) { super(holder, context); } + @Override + protected @NotNull ProblemsHolder getHolder() { + var holder = super.getHolder(); + assert holder != null; + return holder; + } + // TODO: Visit decorators with arguments @Override public void visitPyCallExpression(@NotNull PyCallExpression node) { @@ -83,32 +92,38 @@ public class PyTypeCheckerInspection extends PyInspection { PyAnnotation annotation = function.getAnnotation(); String typeCommentAnnotation = function.getTypeCommentAnnotation(); if (annotation != null || typeCommentAnnotation != null) { + PyType expected = getExpectedReturnType(function, myTypeEvalContext); + if (expected == null) return; + + // We cannot just match annotated and inferred types, as we cannot promote inferred to Literal PyExpression returnExpr = node.getExpression(); - PyType expected = getExpectedReturnType(function); + if (returnExpr == null && !(expected instanceof PyNoneType) && PyTypeChecker.match(expected, PyNoneType.INSTANCE, myTypeEvalContext)) { + final String expectedName = PythonDocumentationProvider.getVerboseTypeName(expected, myTypeEvalContext); + getHolder() + .problem(node, PyPsiBundle.message("INSP.type.checker.returning.type.has.implicit.return", expectedName)) + .fix(new PyMakeReturnsExplicitFix(function)) + .register(); + return; + } + PyType actual = returnExpr != null ? tryPromotingType(returnExpr, expected) : PyNoneType.INSTANCE; - if (expected != null && actual instanceof PyTypedDictType) { + if (actual instanceof PyTypedDictType) { if (reportTypedDictProblems(expected, (PyTypedDictType)actual, returnExpr)) return; } if (!PyTypeChecker.match(expected, actual, myTypeEvalContext)) { - String expectedName = PythonDocumentationProvider.getVerboseTypeName(expected, myTypeEvalContext); - String actualName = PythonDocumentationProvider.getTypeName(actual, myTypeEvalContext); - var localQuickFix = new PyMakeFunctionReturnTypeQuickFix(function, returnExpr, actual, myTypeEvalContext); - var globalQuickFix = new PyMakeFunctionReturnTypeQuickFix(function, returnExpr, null, myTypeEvalContext); - registerProblem(returnExpr != null ? returnExpr : node, - PyPsiBundle.message("INSP.type.checker.expected.type.got.type.instead", expectedName, actualName), - localQuickFix, globalQuickFix); + final String expectedName = PythonDocumentationProvider.getVerboseTypeName(expected, myTypeEvalContext); + final String actualName = PythonDocumentationProvider.getTypeName(actual, myTypeEvalContext); + getHolder() + .problem(returnExpr != null ? returnExpr : node, PyPsiBundle.message("INSP.type.checker.expected.type.got.type.instead", expectedName, actualName)) + .fix(new PyMakeFunctionReturnTypeQuickFix(function, myTypeEvalContext)) + .register(); } } } } - @Nullable - private PyType getExpectedReturnType(@NotNull PyFunction function) { - return getExpectedReturnType(function, myTypeEvalContext); - } - @Nullable public static PyType getExpectedReturnType(@NotNull PyFunction function, @NotNull TypeEvalContext typeEvalContext) { final PyType returnType = typeEvalContext.getReturnType(function); @@ -120,13 +135,6 @@ public class PyTypeCheckerInspection extends PyInspection { return returnType; } - @Nullable - public static PyType getActualReturnType(@NotNull PyFunction function, @Nullable PyExpression returnExpr, - @NotNull TypeEvalContext context) { - PyType returnTypeExpected = getExpectedReturnType(function, context); - return returnExpr != null ? tryPromotingType(returnExpr, returnTypeExpected, context) : PyNoneType.INSTANCE; - } - @Override public void visitPyTargetExpression(@NotNull PyTargetExpression node) { // TODO: Check types in class-level assignments @@ -230,21 +238,28 @@ public class PyTypeCheckerInspection extends PyInspection { final PyAnnotation annotation = node.getAnnotation(); final String typeCommentAnnotation = node.getTypeCommentAnnotation(); if (annotation != null || typeCommentAnnotation != null) { - if (!PyUtil.isEmptyFunction(node)) { - final ReturnVisitor visitor = new ReturnVisitor(node); - node.getStatementList().accept(visitor); - if (!visitor.myHasReturns) { - final PyType expected = getExpectedReturnType(node); - final String expectedName = PythonDocumentationProvider.getTypeName(expected, myTypeEvalContext); - if (expected != null && !(expected instanceof PyNoneType)) { - registerProblem(annotation != null ? annotation.getValue() : node.getTypeComment(), - PyPsiBundle.message("INSP.type.checker.expected.to.return.type.got.no.return", expectedName)); + final PyType expected = getExpectedReturnType(node, myTypeEvalContext); + final boolean returnsNone = expected instanceof PyNoneType; + final boolean returnsOptional = PyTypeChecker.match(expected, PyNoneType.INSTANCE, myTypeEvalContext); + + if (expected != null && !returnsOptional && !PyUtil.isEmptyFunction(node)) { + final List returnPoints = node.getReturnPoints(myTypeEvalContext); + final boolean hasImplicitReturns = exists(returnPoints, it -> !(it instanceof PyReturnStatement)); + + if (hasImplicitReturns) { + final String expectedName = PythonDocumentationProvider.getVerboseTypeName(expected, myTypeEvalContext); + final String actualName = PythonDocumentationProvider.getTypeName(node.getReturnStatementType(myTypeEvalContext), myTypeEvalContext); + final PsiElement annotationValue = annotation != null ? annotation.getValue() : node.getTypeComment(); + if (annotationValue != null) { + getHolder() + .problem(annotationValue, PyPsiBundle.message("INSP.type.checker.expected.type.got.type.instead", expectedName, actualName)) + .fix(new PyMakeFunctionReturnTypeQuickFix(node, myTypeEvalContext)) + .register(); } } } - if (PyUtil.isInitMethod(node) && !(getExpectedReturnType(node) instanceof PyNoneType - || PyTypingTypeProvider.isNoReturn(node, myTypeEvalContext))) { + if (PyUtil.isInitMethod(node) && !(returnsNone || PyTypingTypeProvider.isNoReturn(node, myTypeEvalContext))) { registerProblem(annotation != null ? annotation.getValue() : node.getTypeComment(), PyPsiBundle.message("INSP.type.checker.init.should.return.none")); } @@ -260,29 +275,6 @@ public class PyTypeCheckerInspection extends PyInspection { } } - private static class ReturnVisitor extends PyRecursiveElementVisitor { - private final PyFunction myFunction; - private boolean myHasReturns = false; - - ReturnVisitor(PyFunction function) { - myFunction = function; - } - - @Override - public void visitPyYieldExpression(@NotNull PyYieldExpression node) { - if (ScopeUtil.getScopeOwner(node) == myFunction) { - myHasReturns = true; - } - } - - @Override - public void visitPyReturnStatement(@NotNull PyReturnStatement node) { - if (ScopeUtil.getScopeOwner(node) == myFunction) { - myHasReturns = true; - } - } - } - private void checkCallSite(@NotNull PyCallSiteExpression callSite) { final List calleesResults = StreamEx .of(mapArguments(callSite, getResolveContext())) @@ -514,7 +506,7 @@ public class PyTypeCheckerInspection extends PyInspection { } private static boolean matchedCalleeResultsExist(@NotNull List calleesResults) { - return ContainerUtil.exists(calleesResults, calleeResults -> + return exists(calleesResults, calleeResults -> ContainerUtil.all(calleeResults.getResults(), AnalyzeArgumentResult::isMatched) && calleeResults.getUnmatchedArguments().isEmpty() && calleeResults.getUnmatchedParameters().isEmpty() && diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeFunctionReturnTypeQuickFix.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeFunctionReturnTypeQuickFix.java index 314af46350e3..89593dd53fce 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeFunctionReturnTypeQuickFix.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeFunctionReturnTypeQuickFix.java @@ -32,52 +32,27 @@ import org.jetbrains.annotations.Nullable; import java.util.List; -import static com.jetbrains.python.inspections.PyTypeCheckerInspection.Visitor.getActualReturnType; -/** - * @author lada - */ public class PyMakeFunctionReturnTypeQuickFix implements LocalQuickFix { private final @NotNull SmartPsiElementPointer myFunction; - private final @Nullable SmartPsiElementPointer myReturnExpr; - private final @Nullable SmartPsiElementPointer myAnnotation; - private final @Nullable SmartPsiElementPointer myTypeCommentAnnotation; private final String myReturnTypeName; - private final boolean myHaveSuggestedType; - public PyMakeFunctionReturnTypeQuickFix(@NotNull PyFunction function, - @Nullable PyExpression returnExpr, - @Nullable PyType suggestedReturnType, - @NotNull TypeEvalContext context) { - this(function, - returnExpr, - function.getAnnotation(), - function.getTypeComment(), - suggestedReturnType != null, - getReturnTypeName(function, suggestedReturnType, context)); + public PyMakeFunctionReturnTypeQuickFix(@NotNull PyFunction function, @NotNull TypeEvalContext context) { + this(function, getReturnTypeName(function, context)); + } + + private PyMakeFunctionReturnTypeQuickFix(@NotNull PyFunction function, @NotNull String returnTypeName) { + SmartPointerManager manager = SmartPointerManager.getInstance(function.getProject()); + myFunction = manager.createSmartPsiElementPointer(function); + myReturnTypeName = returnTypeName; } @NotNull - private static String getReturnTypeName(@NotNull PyFunction function, @Nullable PyType returnType, @NotNull TypeEvalContext context) { - PyType type = returnType != null ? returnType : function.getReturnStatementType(context); + private static String getReturnTypeName(@NotNull PyFunction function, @NotNull TypeEvalContext context) { + final PyType type = function.getInferredReturnType(context); return PythonDocumentationProvider.getTypeHint(type, context); } - private PyMakeFunctionReturnTypeQuickFix(@NotNull PyFunction function, - @Nullable PyExpression returnExpr, - @Nullable PyAnnotation annotation, - @Nullable PsiComment typeComment, - boolean returnTypeSuggested, - @NotNull String returnTypeName) { - SmartPointerManager manager = SmartPointerManager.getInstance(function.getProject()); - myFunction = manager.createSmartPsiElementPointer(function); - myReturnExpr = returnExpr != null ? manager.createSmartPsiElementPointer(returnExpr) : null; - myAnnotation = annotation != null ? manager.createSmartPsiElementPointer(annotation) : null; - myTypeCommentAnnotation = typeComment != null ? manager.createSmartPsiElementPointer(typeComment) : null; - myHaveSuggestedType = returnTypeSuggested; - myReturnTypeName = returnTypeName; - } - @Override @NotNull public String getName() { @@ -94,51 +69,46 @@ public class PyMakeFunctionReturnTypeQuickFix implements LocalQuickFix { @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { - PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); - if (myAnnotation != null) { - PyAnnotation annotation = myAnnotation.getElement(); - if (annotation != null) { - PyExpression annotationExpr = annotation.getValue(); - if (annotationExpr == null) return; - PsiElement newElement = - annotationExpr.replace(elementGenerator.createExpressionFromText(LanguageLevel.PYTHON34, myReturnTypeName)); - addImportsForTypeAnnotations(newElement); + final PyFunction function = myFunction.getElement(); + if (function == null) return; + + final PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); + + boolean shouldAddImports = false; + + PyAnnotation annotation = function.getAnnotation(); + if (annotation != null) { + PyExpression annotationExpr = annotation.getValue(); + if (annotationExpr != null) { + annotationExpr.replace(elementGenerator.createExpressionFromText(LanguageLevel.PYTHON34, myReturnTypeName)); + shouldAddImports = true; } } - else if (myTypeCommentAnnotation != null) { - PsiComment typeComment = myTypeCommentAnnotation.getElement(); - if (typeComment != null) { - StringBuilder typeCommentAnnotation = new StringBuilder(typeComment.getText()); - typeCommentAnnotation.delete(typeCommentAnnotation.indexOf("->"), typeCommentAnnotation.length()); - typeCommentAnnotation.append("-> ").append(myReturnTypeName); - PsiComment newTypeComment = - elementGenerator.createFromText(LanguageLevel.PYTHON27, PsiComment.class, typeCommentAnnotation.toString()); - PsiElement newElement = typeComment.replace(newTypeComment); - addImportsForTypeAnnotations(newElement); - } + + PsiComment typeComment = function.getTypeComment(); + if (typeComment != null) { + StringBuilder typeCommentAnnotation = new StringBuilder(typeComment.getText()); + typeCommentAnnotation.delete(typeCommentAnnotation.indexOf("->"), typeCommentAnnotation.length()); + typeCommentAnnotation.append("-> ").append(myReturnTypeName); + typeComment.replace( + elementGenerator.createFromText(LanguageLevel.PYTHON27, PsiComment.class, typeCommentAnnotation.toString())); + shouldAddImports = true; + } + + if (shouldAddImports) { + addImportsForTypeAnnotations(TypeEvalContext.userInitiated(project, function.getContainingFile())); } } - private void addImportsForTypeAnnotations(@NotNull PsiElement element) { - PsiFile file = element.getContainingFile(); - if (file == null) return; + private void addImportsForTypeAnnotations(@NotNull TypeEvalContext context) { PyFunction function = myFunction.getElement(); if (function == null) return; - Project project = element.getProject(); - TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(project, file); - PyType typeForImports = getTypeForImports(function, typeEvalContext); + PsiFile file = function.getContainingFile(); + if (file == null) return; + + PyType typeForImports = function.getInferredReturnType(context); if (typeForImports != null) { - PyTypeHintGenerationUtil.addImportsForTypeAnnotations(List.of(typeForImports), typeEvalContext, file); - } - } - - private @Nullable PyType getTypeForImports(@NotNull PyFunction function, @NotNull TypeEvalContext context) { - PyType returnTypeActual = getActualReturnType(function, myReturnExpr != null ? myReturnExpr.getElement() : null, context); - if (myHaveSuggestedType && returnTypeActual != null) { - return returnTypeActual; - } - else { - return function.getReturnStatementType(context); + PyTypeHintGenerationUtil.addImportsForTypeAnnotations(List.of(typeForImports), context, file); } } @@ -148,18 +118,6 @@ public class PyMakeFunctionReturnTypeQuickFix implements LocalQuickFix { if (function == null) { return null; } - @Nullable PyExpression returnExpr = PyRefactoringUtil.findSameElementForPreview(myReturnExpr, target); - if (myReturnExpr != null && returnExpr == null) { - return null; - } - @Nullable PyAnnotation annotation = PyRefactoringUtil.findSameElementForPreview(myAnnotation, target); - if (myAnnotation != null && annotation == null) { - return null; - } - @Nullable PsiComment typeComment = PyRefactoringUtil.findSameElementForPreview(myTypeCommentAnnotation, target); - if (myTypeCommentAnnotation != null && typeComment == null) { - return null; - } - return new PyMakeFunctionReturnTypeQuickFix(function, returnExpr, annotation, typeComment, myHaveSuggestedType, myReturnTypeName); + return new PyMakeFunctionReturnTypeQuickFix(function, myReturnTypeName); } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnsExplicitFix.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnsExplicitFix.java new file mode 100644 index 000000000000..4f9985bef62d --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnsExplicitFix.java @@ -0,0 +1,46 @@ +package com.jetbrains.python.inspections.quickfix; + +import com.intellij.modcommand.ActionContext; +import com.intellij.modcommand.ModPsiUpdater; +import com.intellij.modcommand.PsiUpdateModCommandAction; +import com.jetbrains.python.PyPsiBundle; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.types.TypeEvalContext; +import org.jetbrains.annotations.NotNull; + +/** + * Appends missing {@code return None}, and transforms {@code return} into {@code return None}. + */ +public class PyMakeReturnsExplicitFix extends PsiUpdateModCommandAction { + + public PyMakeReturnsExplicitFix(@NotNull PyFunction function) { + super(function); + } + + @Override + protected void invoke(@NotNull ActionContext context, @NotNull PyFunction element, @NotNull ModPsiUpdater updater) { + var returnPoints = element.getReturnPoints(TypeEvalContext.userInitiated(element.getProject(), element.getContainingFile())); + for (var point : returnPoints) { + makeExplicit(point); + } + } + + @Override + public @NotNull String getFamilyName() { + return PyPsiBundle.message("QFIX.NAME.make.return.stmts.explicit"); + } + + private static void makeExplicit(@NotNull PyStatement stmt) { + PyElementGenerator elementGenerator = PyElementGenerator.getInstance(stmt.getProject()); + LanguageLevel languageLevel = LanguageLevel.forElement(stmt); + + var returnStmt = elementGenerator.createFromText(languageLevel, PyReturnStatement.class, "return None"); + + if ((stmt instanceof PyReturnStatement ret && ret.getExpression() == null) || (stmt instanceof PyPassStatement)) { + stmt.replace(returnStmt); + } + else if (!(stmt instanceof PyReturnStatement)) { + stmt.getParent().addAfter(returnStmt, stmt); + } + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index 2c8766eb7757..9922e96c0428 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -1,6 +1,8 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.psi.impl; +import com.intellij.codeInsight.controlflow.ControlFlowUtil; +import com.intellij.codeInsight.controlflow.Instruction; import com.intellij.lang.ASTNode; import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.util.Key; @@ -21,6 +23,7 @@ import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.JBIterable; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyStubElementTypes; +import com.jetbrains.python.codeInsight.controlflow.CallInstruction; import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; @@ -153,7 +156,12 @@ public class PyFunctionImpl extends PyBaseElementImpl implements return PyTypingTypeProvider.removeNarrowedTypeIfNeeded(derefType(returnTypeRef, typeProvider)); } } - + + return getInferredReturnType(context); + } + + @Override + public @Nullable PyType getInferredReturnType(@NotNull TypeEvalContext context) { PyType inferredType = null; if (context.allowReturnTypes(this)) { final Ref yieldTypeRef = getYieldStatementType(context); @@ -342,16 +350,54 @@ public class PyFunctionImpl extends PyBaseElementImpl implements @Override public @Nullable PyType getReturnStatementType(@NotNull TypeEvalContext context) { - final ReturnVisitor visitor = new ReturnVisitor(this, context); - final PyStatementList statements = getStatementList(); - statements.accept(visitor); - if ((isGeneratedStub() || PyKnownDecoratorUtil.hasAbstractDecorator(this, context)) && !visitor.myHasReturns) { + return PyUtil.getNullableParameterizedCachedValue(this, context, (it) -> getReturnStatementTypeNoCache(it)); + } + + private @Nullable PyType getReturnStatementTypeNoCache(@NotNull TypeEvalContext context) { + final List returnPoints = getReturnPoints(context); + final List types = new ArrayList<>(); + boolean hasReturn = false; + + for (var point : returnPoints) { + if (point instanceof PyReturnStatement returnStatement) { + hasReturn = true; + final PyExpression expr = returnStatement.getExpression(); + types.add(expr != null ? context.getType(expr) : PyNoneType.INSTANCE); + } + else { + types.add(PyNoneType.INSTANCE); + } + } + + if ((isGeneratedStub() || PyKnownDecoratorUtil.hasAbstractDecorator(this, context)) && !hasReturn) { if (PyUtil.isInitMethod(this)) { return PyNoneType.INSTANCE; } return null; } - return visitor.result(); + return PyUnionType.union(types); + } + + @Override + public @NotNull List getReturnPoints(@NotNull TypeEvalContext context) { + final Instruction[] flow = ControlFlowCache.getControlFlow(this).getInstructions(); + final List returnPoints = new ArrayList<>(); + + ControlFlowUtil.iteratePrev(flow.length-1, flow, instruction -> { + if (instruction instanceof CallInstruction ci && ci.isNoReturnCall(context)) { + return ControlFlowUtil.Operation.CONTINUE; + } + final PsiElement element = instruction.getElement(); + if (!(element instanceof PyStatement statement)) { + return ControlFlowUtil.Operation.NEXT; + } + if (element instanceof PyRaiseStatement) { + return ControlFlowUtil.Operation.CONTINUE; + } + returnPoints.add(statement); + return ControlFlowUtil.Operation.CONTINUE; + }); + return returnPoints; } @Override @@ -412,44 +458,6 @@ public class PyFunctionImpl extends PyBaseElementImpl implements return false; } - private static final class ReturnVisitor extends PyRecursiveElementVisitor { - private final PyFunction myFunction; - private final TypeEvalContext myContext; - private PyType myResult = null; - private boolean myHasReturns = false; - private boolean myHasRaises = false; - - private ReturnVisitor(PyFunction function, final TypeEvalContext context) { - myFunction = function; - myContext = context; - } - - @Override - public void visitPyReturnStatement(@NotNull PyReturnStatement node) { - if (ScopeUtil.getScopeOwner(node) == myFunction) { - final PyExpression expr = node.getExpression(); - PyType returnType = expr == null ? PyNoneType.INSTANCE : myContext.getType(expr); - if (!myHasReturns) { - myResult = returnType; - myHasReturns = true; - } - else { - myResult = PyUnionType.union(myResult, returnType); - } - } - } - - @Override - public void visitPyRaiseStatement(@NotNull PyRaiseStatement node) { - myHasRaises = true; - } - - @Nullable - PyType result() { - return myHasReturns || myHasRaises ? myResult : PyNoneType.INSTANCE; - } - } - @Override protected void acceptPyVisitor(PyElementVisitor pyVisitor) { pyVisitor.visitPyFunction(this); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyPassStatementImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyPassStatementImpl.java index 182b21d084e8..69f42917d8f8 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyPassStatementImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyPassStatementImpl.java @@ -16,6 +16,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; +import com.jetbrains.python.psi.PyElementVisitor; import com.jetbrains.python.psi.PyPassStatement; @@ -23,4 +24,9 @@ public class PyPassStatementImpl extends PyElementImpl implements PyPassStatemen public PyPassStatementImpl(ASTNode astNode) { super(astNode); } + + @Override + protected void acceptPyVisitor(PyElementVisitor pyVisitor) { + pyVisitor.visitPyPassStatement(this); + } } diff --git a/python/testData/codeInsight/controlflow/If.txt b/python/testData/codeInsight/controlflow/If.txt index bcc46e28d089..8368e5037e70 100644 --- a/python/testData/codeInsight/controlflow/If.txt +++ b/python/testData/codeInsight/controlflow/If.txt @@ -1,17 +1,24 @@ 0(1) element: null -1(2,3) element: PyIfStatement +1(2,4) element: PyIfStatement 2(3) element: PyStatementList. Condition: 0j:true -3(4,5) element: PyIfStatement -4(6) element: PyStatementList. Condition: 1j:true -5(6) element: PyStatementList. Condition: 1j:false -6(7,8) element: PyIfStatement -7(10) element: PyStatementList. Condition: 2j:true -8(9,10) element: PyIfPartElif. Condition: 2j:false -9(10) element: PyStatementList. Condition: 3j:true -10(11,12) element: PyIfStatement -11(16) element: PyStatementList. Condition: 4j:true -12(13,14) element: PyIfPartElif. Condition: 4j:false -13(16) element: PyStatementList. Condition: 5j:true -14(15) element: PyStatementList. Condition: 5j:false -15(16) element: PyExpressionStatement -16() element: null \ No newline at end of file +3(4) element: PyPassStatement +4(5,7) element: PyIfStatement +5(6) element: PyStatementList. Condition: 1j:true +6(9) element: PyPassStatement +7(8) element: PyStatementList. Condition: 1j:false +8(9) element: PyPassStatement +9(10,12) element: PyIfStatement +10(11) element: PyStatementList. Condition: 2j:true +11(15) element: PyPassStatement +12(13,15) element: PyIfPartElif. Condition: 2j:false +13(14) element: PyStatementList. Condition: 3j:true +14(15) element: PyPassStatement +15(16,18) element: PyIfStatement +16(17) element: PyStatementList. Condition: 4j:true +17(23) element: PyPassStatement +18(19,21) element: PyIfPartElif. Condition: 4j:false +19(20) element: PyStatementList. Condition: 5j:true +20(23) element: PyPassStatement +21(22) element: PyStatementList. Condition: 5j:false +22(23) element: PyExpressionStatement +23() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/IfElifFalse.txt b/python/testData/codeInsight/controlflow/IfElifFalse.txt index 1cf63c87ea29..4c247c1073d7 100644 --- a/python/testData/codeInsight/controlflow/IfElifFalse.txt +++ b/python/testData/codeInsight/controlflow/IfElifFalse.txt @@ -1,23 +1,26 @@ 0(1) element: null 1(2) element: PyIfStatement -2(3,5) READ ACCESS: c +2(3,6) READ ACCESS: c 3(4) element: PyStatementList. Condition: c:true -4(11) ASSERTTYPE ACCESS: c -5(6) element: PyIfPartElif. Condition: c:false -6(11) READ ACCESS: False -7(8) element: PyStatementList. Condition: False:true -8(9) ASSERTTYPE ACCESS: False -9(10) element: PyAssignmentStatement -10(11) WRITE ACCESS: a -11(12) element: PyIfStatement -12(13,15) READ ACCESS: d -13(14) element: PyStatementList. Condition: d:true -14(22) ASSERTTYPE ACCESS: d -15(16) element: PyIfPartElif. Condition: d:false -16(21) READ ACCESS: False -17(18) element: PyStatementList. Condition: False:true -18(19) ASSERTTYPE ACCESS: False -19(20) element: PyAssignmentStatement -20(22) WRITE ACCESS: b -21(22) element: PyStatementList. Condition: False:false -22() element: null \ No newline at end of file +4(5) ASSERTTYPE ACCESS: c +5(12) element: PyPassStatement +6(7) element: PyIfPartElif. Condition: c:false +7(12) READ ACCESS: False +8(9) element: PyStatementList. Condition: False:true +9(10) ASSERTTYPE ACCESS: False +10(11) element: PyAssignmentStatement +11(12) WRITE ACCESS: a +12(13) element: PyIfStatement +13(14,17) READ ACCESS: d +14(15) element: PyStatementList. Condition: d:true +15(16) ASSERTTYPE ACCESS: d +16(25) element: PyPassStatement +17(18) element: PyIfPartElif. Condition: d:false +18(23) READ ACCESS: False +19(20) element: PyStatementList. Condition: False:true +20(21) ASSERTTYPE ACCESS: False +21(22) element: PyAssignmentStatement +22(25) WRITE ACCESS: b +23(24) element: PyStatementList. Condition: False:false +24(25) element: PyPassStatement +25() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/IfElifTrue.txt b/python/testData/codeInsight/controlflow/IfElifTrue.txt index 2b008aca2df1..0a69a5eb9bda 100644 --- a/python/testData/codeInsight/controlflow/IfElifTrue.txt +++ b/python/testData/codeInsight/controlflow/IfElifTrue.txt @@ -1,21 +1,25 @@ 0(1) element: null 1(2) element: PyIfStatement -2(3,5) READ ACCESS: c +2(3,6) READ ACCESS: c 3(4) element: PyStatementList. Condition: c:true -4(9) ASSERTTYPE ACCESS: c -5(6) element: PyIfPartElif. Condition: c:false -6(7) READ ACCESS: True -7(8) element: PyStatementList. Condition: True:true -8(9) ASSERTTYPE ACCESS: True -9(10) element: PyIfStatement -10(11,13) READ ACCESS: d -11(12) element: PyStatementList. Condition: d:true -12(20) ASSERTTYPE ACCESS: d -13(14) element: PyIfPartElif. Condition: d:false -14(15) READ ACCESS: True -15(16) element: PyStatementList. Condition: True:true -16(20) ASSERTTYPE ACCESS: True -17(18) element: PyStatementList. Condition: True:false -18(19) element: PyAssignmentStatement -19(20) WRITE ACCESS: e -20() element: null \ No newline at end of file +4(5) ASSERTTYPE ACCESS: c +5(11) element: PyPassStatement +6(7) element: PyIfPartElif. Condition: c:false +7(8) READ ACCESS: True +8(9) element: PyStatementList. Condition: True:true +9(10) ASSERTTYPE ACCESS: True +10(11) element: PyPassStatement +11(12) element: PyIfStatement +12(13,16) READ ACCESS: d +13(14) element: PyStatementList. Condition: d:true +14(15) ASSERTTYPE ACCESS: d +15(24) element: PyPassStatement +16(17) element: PyIfPartElif. Condition: d:false +17(18) READ ACCESS: True +18(19) element: PyStatementList. Condition: True:true +19(20) ASSERTTYPE ACCESS: True +20(24) element: PyPassStatement +21(22) element: PyStatementList. Condition: True:false +22(23) element: PyAssignmentStatement +23(24) WRITE ACCESS: e +24() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/IfFalse.txt b/python/testData/codeInsight/controlflow/IfFalse.txt index 5ebb1f5e500a..282f7189d400 100644 --- a/python/testData/codeInsight/controlflow/IfFalse.txt +++ b/python/testData/codeInsight/controlflow/IfFalse.txt @@ -10,17 +10,20 @@ 9(10) element: PyStatementList. Condition: False:true 10(11) ASSERTTYPE ACCESS: False 11(12) element: PyAssignmentStatement -12(14) WRITE ACCESS: b +12(15) WRITE ACCESS: b 13(14) element: PyStatementList. Condition: False:false -14(15) element: PyIfStatement -15(20) READ ACCESS: False -16(17) element: PyStatementList. Condition: False:true -17(18) ASSERTTYPE ACCESS: False -18(19) element: PyAssignmentStatement -19(25) WRITE ACCESS: c -20(21) element: PyIfPartElif. Condition: False:false -21(22,24) READ ACCESS: d -22(23) element: PyStatementList. Condition: d:true -23(25) ASSERTTYPE ACCESS: d -24(25) element: PyStatementList. Condition: d:false -25() element: null \ No newline at end of file +14(15) element: PyPassStatement +15(16) element: PyIfStatement +16(21) READ ACCESS: False +17(18) element: PyStatementList. Condition: False:true +18(19) ASSERTTYPE ACCESS: False +19(20) element: PyAssignmentStatement +20(28) WRITE ACCESS: c +21(22) element: PyIfPartElif. Condition: False:false +22(23,26) READ ACCESS: d +23(24) element: PyStatementList. Condition: d:true +24(25) ASSERTTYPE ACCESS: d +25(28) element: PyPassStatement +26(27) element: PyStatementList. Condition: d:false +27(28) element: PyPassStatement +28() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/IfTrue.txt b/python/testData/codeInsight/controlflow/IfTrue.txt index e208e361db0d..5c4bb7a18aaa 100644 --- a/python/testData/codeInsight/controlflow/IfTrue.txt +++ b/python/testData/codeInsight/controlflow/IfTrue.txt @@ -3,24 +3,27 @@ 2(3) READ ACCESS: True 3(4) element: PyStatementList. Condition: True:true 4(5) ASSERTTYPE ACCESS: True -5(6) element: PyIfStatement -6(7) READ ACCESS: True -7(8) element: PyStatementList. Condition: True:true -8(12) ASSERTTYPE ACCESS: True -9(10) element: PyStatementList. Condition: True:false -10(11) element: PyAssignmentStatement -11(12) WRITE ACCESS: b -12(13) element: PyIfStatement -13(14) READ ACCESS: True -14(15) element: PyStatementList. Condition: True:true -15(25) ASSERTTYPE ACCESS: True -16(17) element: PyIfPartElif. Condition: True:false -17(18) READ ACCESS: c -18(19) element: PyStatementList. Condition: c:true -19(20) ASSERTTYPE ACCESS: c -20(21) element: PyAssignmentStatement -21(25) WRITE ACCESS: d -22(23) element: PyStatementList. Condition: c:false +5(6) element: PyPassStatement +6(7) element: PyIfStatement +7(8) READ ACCESS: True +8(9) element: PyStatementList. Condition: True:true +9(10) ASSERTTYPE ACCESS: True +10(14) element: PyPassStatement +11(12) element: PyStatementList. Condition: True:false +12(13) element: PyAssignmentStatement +13(14) WRITE ACCESS: b +14(15) element: PyIfStatement +15(16) READ ACCESS: True +16(17) element: PyStatementList. Condition: True:true +17(18) ASSERTTYPE ACCESS: True +18(28) element: PyPassStatement +19(20) element: PyIfPartElif. Condition: True:false +20(21) READ ACCESS: c +21(22) element: PyStatementList. Condition: c:true +22(23) ASSERTTYPE ACCESS: c 23(24) element: PyAssignmentStatement -24(25) WRITE ACCESS: e -25() element: null \ No newline at end of file +24(28) WRITE ACCESS: d +25(26) element: PyStatementList. Condition: c:false +26(27) element: PyAssignmentStatement +27(28) WRITE ACCESS: e +28() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/Pass.py b/python/testData/codeInsight/controlflow/Pass.py new file mode 100644 index 000000000000..fc80254b619d --- /dev/null +++ b/python/testData/codeInsight/controlflow/Pass.py @@ -0,0 +1 @@ +pass \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/Pass.txt b/python/testData/codeInsight/controlflow/Pass.txt new file mode 100644 index 000000000000..dc4b231f1641 --- /dev/null +++ b/python/testData/codeInsight/controlflow/Pass.txt @@ -0,0 +1,3 @@ +0(1) element: null +1(2) element: PyPassStatement +2() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeGuard.txt b/python/testData/codeInsight/controlflow/TypeGuard.txt index 7122cec9ff16..68cccfa950a6 100644 --- a/python/testData/codeInsight/controlflow/TypeGuard.txt +++ b/python/testData/codeInsight/controlflow/TypeGuard.txt @@ -25,6 +25,7 @@ 24(25,28) element: PyCallExpression: checkit 25(26) element: PyStatementList. Condition: checkit(x):true 26(27) element: PyPrintStatement -27(29) READ ACCESS: x +27(30) READ ACCESS: x 28(29) element: PyStatementList. Condition: checkit(x):false -29() element: null \ No newline at end of file +29(30) element: PyPassStatement +30() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeParameterListInClassDeclaration.txt b/python/testData/codeInsight/controlflow/TypeParameterListInClassDeclaration.txt index c6b3eaaaf698..1387f6362f3c 100644 --- a/python/testData/codeInsight/controlflow/TypeParameterListInClassDeclaration.txt +++ b/python/testData/codeInsight/controlflow/TypeParameterListInClassDeclaration.txt @@ -6,4 +6,5 @@ 5(6) element: PySubscriptionExpression 6(7) READ ACCESS: BaseClass 7(8) READ ACCESS: T -8() element: null \ No newline at end of file +8(9) element: PyPassStatement +9() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt b/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt index 4437ca6dbbb8..d55fa5031f7e 100644 --- a/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt +++ b/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt @@ -7,4 +7,5 @@ 6(7) WRITE ACCESS: a 7(8) READ ACCESS: U 8(9) WRITE ACCESS: b -9() element: null \ No newline at end of file +9(10) element: PyPassStatement +10() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypesInAndBooleanExpression.txt b/python/testData/codeInsight/controlflow/TypesInAndBooleanExpression.txt index d2c647c50995..c462a2779172 100644 --- a/python/testData/codeInsight/controlflow/TypesInAndBooleanExpression.txt +++ b/python/testData/codeInsight/controlflow/TypesInAndBooleanExpression.txt @@ -11,13 +11,14 @@ 10(11) READ ACCESS: var 11(12) READ ACCESS: A 12(13,14) element: PyCallExpression: isinstance -13(21) element: null. Condition: isinstance(var, A):false +13(22) element: null. Condition: isinstance(var, A):false 14(15) element: null. Condition: isinstance(var, A):true 15(16) ASSERTTYPE ACCESS: var 16(17,18) READ ACCESS: var -17(21) element: null. Condition: var:false +17(22) element: null. Condition: var:false 18(19) element: null. Condition: var:true 19(20) element: PyStatementList. Condition: isinstance(var, A) and var:true -20(22) ASSERTTYPE ACCESS: var -21(22) ASSERTTYPE ACCESS: var -22() element: null \ No newline at end of file +20(21) ASSERTTYPE ACCESS: var +21(23) element: PyPassStatement +22(23) ASSERTTYPE ACCESS: var +23() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypesInOrBooleanExpression.txt b/python/testData/codeInsight/controlflow/TypesInOrBooleanExpression.txt index 7514fc72ec2f..b276c14a5b4b 100644 --- a/python/testData/codeInsight/controlflow/TypesInOrBooleanExpression.txt +++ b/python/testData/codeInsight/controlflow/TypesInOrBooleanExpression.txt @@ -7,7 +7,7 @@ 6(7) READ ACCESS: isinstance 7(8) READ ACCESS: var 8(9) READ ACCESS: A -9(10,27) element: PyCallExpression: isinstance +9(10,28) element: PyCallExpression: isinstance 10(11) element: PyStatementList. Condition: isinstance(var, A):true 11(12) ASSERTTYPE ACCESS: var 12(13) element: PyIfStatement @@ -20,10 +20,11 @@ 19(20) element: null. Condition: isinstance(var, B):false 20(21) ASSERTTYPE ACCESS: var 21(22,23) READ ACCESS: var -22(26) element: null. Condition: var:false +22(27) element: null. Condition: var:false 23(24) element: null. Condition: var:true 24(25) element: PyStatementList. Condition: isinstance(var, B) or var:true -25(28) ASSERTTYPE ACCESS: var -26(28) ASSERTTYPE ACCESS: var -27(28) ASSERTTYPE ACCESS: var -28() element: null \ No newline at end of file +25(26) ASSERTTYPE ACCESS: var +26(29) element: PyPassStatement +27(29) ASSERTTYPE ACCESS: var +28(29) ASSERTTYPE ACCESS: var +29() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/WhileTrueElse.txt b/python/testData/codeInsight/controlflow/WhileTrueElse.txt index 9d9fabf147ce..689e13fefcb4 100644 --- a/python/testData/codeInsight/controlflow/WhileTrueElse.txt +++ b/python/testData/codeInsight/controlflow/WhileTrueElse.txt @@ -1,7 +1,8 @@ 0(1) element: null 1(2) element: PyWhileStatement 2(3) READ ACCESS: True -3(1) element: PyStatementList. Condition: True:true -4(5) element: PyElsePart. Condition: True:false -5(6) element: PyPrintStatement -6() element: null \ No newline at end of file +3(4) element: PyStatementList. Condition: True:true +4(1) element: PyPassStatement +5(6) element: PyElsePart. Condition: True:false +6(7) element: PyPrintStatement +7() element: null \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py b/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py index 50ef2a06ff87..e456889ddd2d 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py +++ b/python/testData/inspections/PyTypeCheckerInspection/FunctionReturnTypePy3.py @@ -22,7 +22,7 @@ def f() -> Optional[str]: elif x == 0: return 'abc' else: - return + return def g(x) -> int: if x: @@ -36,9 +36,18 @@ def h(x) -> int: def i() -> Union[int, str]: pass -def j(x) -> Union[int, str]: +def j(x) -> Union[int, str]: x = 42 def k() -> None: if True: - pass \ No newline at end of file + pass + +def l(x) -> int: + if x == 1: + return 42 + +def m(x) -> None: + """Does not display warning about implicit return, because annotated '-> None' """ + if x: + return \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/TypedDictInReturnType.py b/python/testData/inspections/PyTypeCheckerInspection/TypedDictInReturnType.py index a7f2c9af5cec..60f827f2d858 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/TypedDictInReturnType.py +++ b/python/testData/inspections/PyTypeCheckerInspection/TypedDictInReturnType.py @@ -40,9 +40,9 @@ def g() -> Point: else: return -def h(x) -> Point: +def h(x) -> Point: x = 42 -def i() -> Point: +def i() -> Point: if True: pass \ No newline at end of file diff --git a/python/testData/quickFixes/PyMakeFunctionReturnTypeQuickFixTest/ancestorAndInheritorReturn/main_after.py b/python/testData/quickFixes/PyMakeFunctionReturnTypeQuickFixTest/ancestorAndInheritorReturn/main_after.py index 8e64a8c012eb..f802da94d2e2 100644 --- a/python/testData/quickFixes/PyMakeFunctionReturnTypeQuickFixTest/ancestorAndInheritorReturn/main_after.py +++ b/python/testData/quickFixes/PyMakeFunctionReturnTypeQuickFixTest/ancestorAndInheritorReturn/main_after.py @@ -1,10 +1,10 @@ from typing import Type import my -from my import X +from my import X, Y -def foo(a) -> Type[X]: +def foo(a) -> Type[X | Y]: if a: return my.X else: diff --git a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt.py b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt.py new file mode 100644 index 000000000000..306d8cf937a9 --- /dev/null +++ b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt.py @@ -0,0 +1,7 @@ +def f(x) -> int | None: + if x == 1: + return 42 + elif x == 2: + return + elif x == 3: + pass \ No newline at end of file diff --git a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt_after.py b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt_after.py new file mode 100644 index 000000000000..28ee5dbab6b7 --- /dev/null +++ b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt_after.py @@ -0,0 +1,8 @@ +def f(x) -> int | None: + if x == 1: + return 42 + elif x == 2: + return None + elif x == 3: + return None + return None \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java index 25b858243d2a..7af02903e5d5 100644 --- a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java +++ b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java @@ -42,6 +42,10 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase { doTest(); } + public void testPass() { + doTest(); + } + public void testFile() { doTest(); } diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyMakeFunctionReturnTypeQuickFixTest.java b/python/testSrc/com/jetbrains/python/quickFixes/PyMakeFunctionReturnTypeQuickFixTest.java index 2562358a3404..3194c6a55df9 100644 --- a/python/testSrc/com/jetbrains/python/quickFixes/PyMakeFunctionReturnTypeQuickFixTest.java +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyMakeFunctionReturnTypeQuickFixTest.java @@ -55,7 +55,7 @@ public class PyMakeFunctionReturnTypeQuickFixTest extends PyQuickFixTestCase { // PY-27128 public void testAncestorAndInheritorReturn() { - doMultiFileTest(PyTypeCheckerInspection.class, PyPsiBundle.message("QFIX.make.function.return.type", "foo", "Type[X]")); + doMultiFileTest(PyTypeCheckerInspection.class, PyPsiBundle.message("QFIX.make.function.return.type", "foo", "Type[X | Y]")); } // PY-27128 PY-48466 diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnsExplicitFixTest.java b/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnsExplicitFixTest.java new file mode 100644 index 000000000000..3f643501679e --- /dev/null +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnsExplicitFixTest.java @@ -0,0 +1,15 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.quickFixes; + +import com.intellij.testFramework.TestDataPath; +import com.jetbrains.python.PyPsiBundle; +import com.jetbrains.python.PyQuickFixTestCase; +import com.jetbrains.python.inspections.PyTypeCheckerInspection; + +@TestDataPath("$CONTENT_ROOT/../testData/quickFixes/PyMakeReturnsExplicitFixTest/") +public class PyMakeReturnsExplicitFixTest extends PyQuickFixTestCase { + + public void testAddReturnsFromReturnStmt() { + doQuickFixTest(PyTypeCheckerInspection.class, PyPsiBundle.message("QFIX.NAME.make.return.stmts.explicit")); + } +}