diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/PyWithItem.java b/python/python-psi-api/src/com/jetbrains/python/psi/PyWithItem.java index 4437e3d4f5da..e2dd9635ba6e 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/PyWithItem.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/PyWithItem.java @@ -2,6 +2,7 @@ package com.jetbrains.python.psi; import com.jetbrains.python.ast.PyAstWithItem; +import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -18,4 +19,6 @@ public interface PyWithItem extends PyAstWithItem, PyElement { default @Nullable PyExpression getTarget() { return (PyExpression)PyAstWithItem.super.getTarget(); } + + boolean isSuppressingExceptions(TypeEvalContext context); } diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/PyWithStatement.java b/python/python-psi-api/src/com/jetbrains/python/psi/PyWithStatement.java index e1e902ca27e4..345f942cb8d6 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/PyWithStatement.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/PyWithStatement.java @@ -3,6 +3,7 @@ package com.jetbrains.python.psi; import com.jetbrains.python.ast.PyAstWithStatement; +import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; public interface PyWithStatement extends PyAstWithStatement, PyCompoundStatement, PyNamedElementContainer, PyStatementListContainer { @@ -13,4 +14,6 @@ public interface PyWithStatement extends PyAstWithStatement, PyCompoundStatement @Override PyWithItem[] getWithItems(); + + boolean isSuppressingExceptions(TypeEvalContext context); } diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 697f3b4a6eda..7c7a1b551809 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -470,7 +470,8 @@ 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 +QFIX.add.explicit.return.none=Add explicit 'return None' +QFIX.replace.with.return.none=Replace with 'return None' # Add method quick-fix QFIX.NAME.add.method.to.class=Add method to class @@ -1023,8 +1024,8 @@ INSP.inconsistent.indentation.previous.line.used.spaces.this.line.uses.tabs=Inco # PyInconsistentReturnsInspection INSP.NAME.inconsistent.returns=Inconsistent return statements -INSP.inconsistent.returns.stmt.expected=Explicit return statement expected -INSP.inconsistent.returns.value.expected=Explicit return value expected +INSP.inconsistent.returns.missing.return.stmt.on.some.paths=Missing return statement on some paths +INSP.inconsistent.returns.return.without.value='return' without value is inconsistent with other paths # PyMissingTypeHintsInspection diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt index c02e98db8a0b..5e4a9b2b4b47 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt @@ -2,14 +2,7 @@ package com.jetbrains.python.codeInsight.controlflow import com.intellij.codeInsight.controlflow.ControlFlowBuilder import com.intellij.codeInsight.controlflow.impl.InstructionImpl -import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.python.psi.PyWithItem -import com.jetbrains.python.psi.PyWithStatement -import com.jetbrains.python.psi.impl.PyBuiltinCache -import com.jetbrains.python.psi.impl.PyEvaluator -import com.jetbrains.python.psi.types.PyCollectionType -import com.jetbrains.python.psi.types.PyLiteralType -import com.jetbrains.python.psi.types.PyTypeUtil import com.jetbrains.python.psi.types.TypeEvalContext class PyWithContextExitInstruction(builder: ControlFlowBuilder, withItem: PyWithItem): InstructionImpl(builder, withItem) { @@ -20,13 +13,5 @@ class PyWithContextExitInstruction(builder: ControlFlowBuilder, withItem: PyWith * While traversing CFG, use this method to know if you should let your traversal consider this node. * Usually, you would want it only if the context manager DOES suppress exceptions. */ - fun isSuppressingExceptions(context: TypeEvalContext): Boolean { - val withStmt = PsiTreeUtil.getParentOfType(element, PyWithStatement::class.java, false) ?: return false - val abstractType = if (withStmt.isAsync) "contextlib.AbstractAsyncContextManager" else "contextlib.AbstractContextManager" - return context.getType(element.expression) - .let { PyTypeUtil.convertToType(it, abstractType, element, context) } - .let { (it as? PyCollectionType)?.elementTypes?.getOrNull(1) } - .let { it == PyBuiltinCache.getInstance(element).boolType || - it is PyLiteralType && PyEvaluator.getBooleanLiteralValue(it.expression) == true } - } + fun isSuppressingExceptions(context: TypeEvalContext): Boolean = element.isSuppressingExceptions(context) } \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/extensions/PyMatchStatementExt.kt b/python/python-psi-impl/src/com/jetbrains/python/extensions/PyMatchStatementExt.kt new file mode 100644 index 000000000000..a1634533a821 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/extensions/PyMatchStatementExt.kt @@ -0,0 +1,9 @@ +package com.jetbrains.python.extensions + +import com.jetbrains.python.psi.PyMatchStatement +import com.jetbrains.python.psi.types.TypeEvalContext + +fun PyMatchStatement.isExhaustive(context: TypeEvalContext): Boolean { + // TODO: placeholder until exhaustive match is introduced + return caseClauses.any { it.pattern?.isIrrefutable == true } +} \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInconsistentReturnsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInconsistentReturnsInspection.kt index 96ba34917adc..162da0f313d9 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInconsistentReturnsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInconsistentReturnsInspection.kt @@ -1,12 +1,18 @@ - package com.jetbrains.python.inspections +package com.jetbrains.python.inspections import com.intellij.codeInspection.LocalInspectionToolSession import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.util.findParentInFile +import com.intellij.psi.util.parentOfType import com.jetbrains.python.PyPsiBundle -import com.jetbrains.python.inspections.quickfix.PyMakeReturnsExplicitFix -import com.jetbrains.python.psi.PyFunction -import com.jetbrains.python.psi.PyReturnStatement +import com.jetbrains.python.codeInsight.controlflow.ScopeOwner +import com.jetbrains.python.extensions.isExhaustive +import com.jetbrains.python.inspections.quickfix.PyMakeReturnExplicitFix +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.types.TypeEvalContext /** * PEP8: @@ -14,26 +20,54 @@ import com.jetbrains.python.psi.PyReturnStatement * or none of them should. If any return statement returns an expression, any return statements where no value * is returned should explicitly state this as return None, and an explicit return statement should be present * at the end of the function (if reachable). - * - * @see PyMakeReturnsExplicitFix for tests */ class PyInconsistentReturnsInspection : PyInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { return object : PyInspectionVisitor(holder, getContext(session)) { override fun visitPyFunction(node: PyFunction) { val returnPoints = node.getReturnPoints(myTypeEvalContext) - val hasExplicitReturns = returnPoints.any { (it as? PyReturnStatement)?.expression != null } - if (hasExplicitReturns) { - returnPoints - .filter { (it !is PyReturnStatement || it.expression == null) } - .forEach { - val message = - if (it is PyReturnStatement) PyPsiBundle.message("INSP.inconsistent.returns.value.expected") - else PyPsiBundle.message("INSP.inconsistent.returns.stmt.expected") - this.holder!!.problem(it, message).fix(PyMakeReturnsExplicitFix(node)).register() - } + val hasExplicitReturns = returnPoints.any { it is PyReturnStatement && it.expression != null } + if (!hasExplicitReturns) return + + for (statement in returnPoints) { + val message = when { + statement is PyReturnStatement && statement.expression == null -> + PyPsiBundle.message("INSP.inconsistent.returns.return.without.value") + + statement !is PyReturnStatement && !isTooComplexCase(statement, myTypeEvalContext) -> + PyPsiBundle.message("INSP.inconsistent.returns.missing.return.stmt.on.some.paths") + + else -> continue + } + + this.holder!! + .problem(statement, message) + .range(statement.firstLineRange() ?: statement.textRange) + .fix(PyMakeReturnExplicitFix(statement)).register() } } } } +} + +private fun isTooComplexCase(statement: PyStatement, context: TypeEvalContext): Boolean { + val parent = statement.findParentInFile(withSelf = false) { + it is ScopeOwner + || it is PyTryExceptStatement + || it is PyForPart + || it is PyWhilePart + || it is PyIfPart && it.parentOfType()?.elsePart == null + || (it is PyWithStatement && it.isSuppressingExceptions(context)) + || (it is PyMatchStatement && !it.isExhaustive(context)) + } + return parent !is ScopeOwner +} + +private fun PsiElement.firstLineRange(): TextRange? { + val document = containingFile.fileDocument + val startOffset = textRange.startOffset + val lineEndOffset = document.getLineEndOffset(document.getLineNumber(startOffset)) + val endOffset = minOf(lineEndOffset, textRange.endOffset) + + return TextRange(0, endOffset - startOffset) } \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnExplicitFix.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnExplicitFix.kt new file mode 100644 index 000000000000..9d33cbb8ef77 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnExplicitFix.kt @@ -0,0 +1,37 @@ +package com.jetbrains.python.inspections.quickfix + +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.modcommand.ActionContext +import com.intellij.modcommand.ModPsiUpdater +import com.intellij.modcommand.Presentation +import com.intellij.modcommand.PsiUpdateModCommandAction +import com.jetbrains.python.PyPsiBundle +import com.jetbrains.python.psi.* + +/** + * Appends missing `return None`, and transforms `return` into `return None`. + */ +class PyMakeReturnExplicitFix(statement: PyStatement) : PsiUpdateModCommandAction(statement) { + override fun getFamilyName(): @IntentionFamilyName String = PyPsiBundle.message("QFIX.add.explicit.return.none") + + override fun getPresentation(context: ActionContext, element: PyStatement): Presentation? { + if (element is PyReturnStatement || element is PyPassStatement) { + return Presentation.of(PyPsiBundle.message("QFIX.replace.with.return.none")) + } + return super.getPresentation(context, element) + } + + override fun invoke(context: ActionContext, element: PyStatement, updater: ModPsiUpdater) { + val elementGenerator = PyElementGenerator.getInstance(element.getProject()) + val languageLevel = LanguageLevel.forElement(element) + + val returnStmt = elementGenerator.createFromText(languageLevel, PyReturnStatement::class.java, "return None") + + if (element is PyReturnStatement || element is PyPassStatement) { + element.replace(returnStmt) + } + else { + element.parent.addAfter(returnStmt, element) + } + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnsExplicitFix.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnsExplicitFix.kt deleted file mode 100644 index 4f9985bef62d..000000000000 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyMakeReturnsExplicitFix.kt +++ /dev/null @@ -1,46 +0,0 @@ -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/PyWithItemImpl.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyWithItemImpl.kt index caf08e0ebb76..7309a2e96598 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyWithItemImpl.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyWithItemImpl.kt @@ -13,20 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.psi.impl; +package com.jetbrains.python.psi.impl -import com.intellij.lang.ASTNode; -import com.jetbrains.python.psi.PyElementVisitor; -import com.jetbrains.python.psi.PyWithItem; +import com.intellij.lang.ASTNode +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.psi.PyElementVisitor +import com.jetbrains.python.psi.PyWithItem +import com.jetbrains.python.psi.PyWithStatement +import com.jetbrains.python.psi.types.PyCollectionType +import com.jetbrains.python.psi.types.PyLiteralType +import com.jetbrains.python.psi.types.PyTypeUtil +import com.jetbrains.python.psi.types.TypeEvalContext - -public class PyWithItemImpl extends PyElementImpl implements PyWithItem { - public PyWithItemImpl(ASTNode astNode) { - super(astNode); +class PyWithItemImpl(astNode: ASTNode?) : PyElementImpl(astNode), PyWithItem { + override fun acceptPyVisitor(pyVisitor: PyElementVisitor) { + pyVisitor.visitPyWithItem(this) } - @Override - protected void acceptPyVisitor(PyElementVisitor pyVisitor) { - pyVisitor.visitPyWithItem(this); + override fun isSuppressingExceptions(context: TypeEvalContext): Boolean { + val withStmt = PsiTreeUtil.getParentOfType(this, PyWithStatement::class.java, false) ?: return false + val abstractType = if (withStmt.isAsync) "contextlib.AbstractAsyncContextManager" else "contextlib.AbstractContextManager" + return context.getType(expression) + .let { PyTypeUtil.convertToType(it, abstractType, this, context) } + .let { (it as? PyCollectionType)?.elementTypes?.getOrNull(1) } + .let { + it == PyBuiltinCache.getInstance(this).boolType || + it is PyLiteralType && PyEvaluator.getBooleanLiteralValue(it.expression) == true + } } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyWithStatementImpl.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyWithStatementImpl.kt index acaf19e905c2..c33a117595c6 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyWithStatementImpl.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyWithStatementImpl.kt @@ -1,42 +1,34 @@ // 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; +package com.jetbrains.python.psi.impl -import com.intellij.lang.ASTNode; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiListLikeElement; -import com.intellij.psi.PsiNamedElement; -import com.jetbrains.python.psi.PyElementVisitor; -import com.jetbrains.python.psi.PyUtil; -import com.jetbrains.python.psi.PyWithItem; -import com.jetbrains.python.psi.PyWithStatement; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiListLikeElement +import com.intellij.psi.PsiNamedElement +import com.jetbrains.python.ast.PyAstWithStatement +import com.jetbrains.python.psi.PyElementVisitor +import com.jetbrains.python.psi.PyUtil +import com.jetbrains.python.psi.PyWithItem +import com.jetbrains.python.psi.PyWithStatement +import com.jetbrains.python.psi.types.TypeEvalContext -import java.util.Arrays; -import java.util.List; - - -public class PyWithStatementImpl extends PyElementImpl implements PyWithStatement, PsiListLikeElement { - public PyWithStatementImpl(ASTNode astNode) { - super(astNode); +class PyWithStatementImpl(astNode: ASTNode?) : PyElementImpl(astNode), PyWithStatement, PsiListLikeElement { + override fun acceptPyVisitor(pyVisitor: PyElementVisitor) { + pyVisitor.visitPyWithStatement(this) } - @Override - protected void acceptPyVisitor(final PyElementVisitor pyVisitor) { - pyVisitor.visitPyWithStatement(this); + fun getNamedElement(the_name: String): PsiNamedElement? { + return PyUtil.IterHelper.findName(getNamedElements(), the_name) } - public @Nullable PsiNamedElement getNamedElement(final @NotNull String the_name) { - return PyUtil.IterHelper.findName(getNamedElements(), the_name); + override fun getWithItems(): Array { + return childrenToPsi(PyAstWithStatement.WITH_ITEM, PyWithItem.EMPTY_ARRAY) } - @Override - public PyWithItem[] getWithItems() { - return childrenToPsi(WITH_ITEM, PyWithItem.EMPTY_ARRAY); + override fun getComponents(): List { + return withItems.toList() } - @Override - public @NotNull List getComponents() { - return Arrays.asList(getWithItems()); + override fun isSuppressingExceptions(context: TypeEvalContext): Boolean { + return withItems.any { it.isSuppressingExceptions(context) } } } diff --git a/python/testData/quickFixes/PyMakeReturnExplicitFixTest/addExplicitReturn.py b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/addExplicitReturn.py new file mode 100644 index 000000000000..ee6e005ddfc5 --- /dev/null +++ b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/addExplicitReturn.py @@ -0,0 +1,5 @@ +def func(x, y, z): + if x: + return 42 + elif y: + return diff --git a/python/testData/quickFixes/PyMakeReturnExplicitFixTest/addExplicitReturn_after.py b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/addExplicitReturn_after.py new file mode 100644 index 000000000000..2a53a59ca99c --- /dev/null +++ b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/addExplicitReturn_after.py @@ -0,0 +1,6 @@ +def func(x, y, z): + if x: + return 42 + elif y: + return + return None diff --git a/python/testData/quickFixes/PyMakeReturnExplicitFixTest/replaceWithReturnNone.py b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/replaceWithReturnNone.py new file mode 100644 index 000000000000..2429ce6025a8 --- /dev/null +++ b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/replaceWithReturnNone.py @@ -0,0 +1,5 @@ +def func(x, y, z): + if x: + return 42 + elif y: + return diff --git a/python/testData/quickFixes/PyMakeReturnExplicitFixTest/replaceWithReturnNone_after.py b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/replaceWithReturnNone_after.py new file mode 100644 index 000000000000..ec55974281bd --- /dev/null +++ b/python/testData/quickFixes/PyMakeReturnExplicitFixTest/replaceWithReturnNone_after.py @@ -0,0 +1,5 @@ +def func(x, y, z): + if x: + return 42 + elif y: + return None diff --git a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt.py b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt.py deleted file mode 100644 index 93a7c064d0e7..000000000000 --- a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt.py +++ /dev/null @@ -1,15 +0,0 @@ -def f(x) -> int | None: - y = 42 - print(y) - if x == 1: - return 42 - elif x == 2: - return - elif x == 3: - raise Exception() - elif x == 4: - assert False - elif x == 5: - assert x - elif x == 4: - 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 deleted file mode 100644 index 1683c4b60df3..000000000000 --- a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/addReturnsFromReturnStmt_after.py +++ /dev/null @@ -1,17 +0,0 @@ -def f(x) -> int | None: - y = 42 - print(y) - if x == 1: - return 42 - elif x == 2: - return None - elif x == 3: - raise Exception() - elif x == 4: - assert False - elif x == 5: - assert x - return None - elif x == 4: - return None - return None \ No newline at end of file diff --git a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerNotSuppressingException.py b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerNotSuppressingException.py deleted file mode 100644 index 2b5accae7edd..000000000000 --- a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerNotSuppressingException.py +++ /dev/null @@ -1,11 +0,0 @@ -class NotSuppressingContext: - def __enter__(self): - ... - def __exit__(self, exc_type, exc_val, exc_tb) -> bool | None: - ... - -def foo(): - with NotSuppressingContext() as st: - foo() - if bool(): - return 1 \ No newline at end of file diff --git a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerNotSuppressingException_after.py b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerNotSuppressingException_after.py deleted file mode 100644 index c0c97aacf1e9..000000000000 --- a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerNotSuppressingException_after.py +++ /dev/null @@ -1,12 +0,0 @@ -class NotSuppressingContext: - def __enter__(self): - ... - def __exit__(self, exc_type, exc_val, exc_tb) -> bool | None: - ... - -def foo(): - with NotSuppressingContext() as st: - foo() - if bool(): - return 1 - return None \ No newline at end of file diff --git a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerSuppressingException.py b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerSuppressingException.py deleted file mode 100644 index 512095e3cb86..000000000000 --- a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerSuppressingException.py +++ /dev/null @@ -1,10 +0,0 @@ -class SuppressingContext: - def __enter__(self): - ... - def __exit__(self, exc_type, exc_val, exc_tb) -> bool: - ... - -def foo(): - with SuppressingContext() as st: - foo() - return 1 \ No newline at end of file diff --git a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerSuppressingException_after.py b/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerSuppressingException_after.py deleted file mode 100644 index 480e471506a8..000000000000 --- a/python/testData/quickFixes/PyMakeReturnsExplicitFixTest/contextManagerSuppressingException_after.py +++ /dev/null @@ -1,11 +0,0 @@ -class SuppressingContext: - def __enter__(self): - ... - def __exit__(self, exc_type, exc_val, exc_tb) -> bool: - ... - -def foo(): - with SuppressingContext() as st: - foo() - return 1 - return None \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyInconsistentReturnsInspectionTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyInconsistentReturnsInspectionTest.kt new file mode 100644 index 000000000000..baf352fabb5f --- /dev/null +++ b/python/testSrc/com/jetbrains/python/inspections/PyInconsistentReturnsInspectionTest.kt @@ -0,0 +1,278 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.inspections + +import com.jetbrains.python.fixtures.PyInspectionTestCase + +class PyInconsistentReturnsInspectionTest : PyInspectionTestCase() { + override fun getInspectionClass(): Class = PyInconsistentReturnsInspection::class.java + override fun doTestByText(text: String) = super.doTestByText(text.trimIndent()) + + // -------------------------- + // Basic Return Tests + // -------------------------- + + fun testNoReturns() = doTestByText(""" + def f(): + x = 1 + """) + + fun testOnlyExplicitReturn() = doTestByText(""" + def f(): + return 42 + """) + + fun testOnlyImplicitNoneNoExplicit() = doTestByText(""" + def f(): + return + """) + + fun testExplicitReturnAndImplicitNone() = doTestByText(""" + def f(cond): + if cond: + return 10 + return + """) + + fun testExplicitReturnAndImplicitStmt() = doTestByText(""" + def f(cond): + if cond: + return 1 + """) + + // -------------------------- + // If Tests + // -------------------------- + + fun testIfElse() = doTestByText(""" + def f(cond, cond2, cond3): + if cond: + return 'yes' + elif cond2: + return + elif cond3: + print() + else: + return 'no' + """) + + fun testIfNoElse() = doTestByText(""" + def f(cond, cond2, cond3): + if cond: + return 'yes' + elif cond2: + return + elif cond3: + print() + """) + + // -------------------------- + // While Tests + // -------------------------- + + fun testWhileTrueNoWarning() = doTestByText(""" + def f(): + while True: + do_something() + """) + + fun testWhile() = doTestByText(""" + def f(): + if x: + return 1 + while not x: + break + """) + + fun testWhileImplicitNone() = doTestByText(""" + def f(): + if x: + return 1 + while not x: + return + """) + + fun testWhileWithElse() = doTestByText(""" + def f(): + if x: + return 1 + while not x: + return + else: + x = 5 + """) + + // -------------------------- + // For Tests + // -------------------------- + + fun testForNoElse() = doTestByText(""" + def f(x): + if x: + return 1 + for i in range(10): + if i > 10: + return + """) + + fun testForLoopIncomplete() = doTestByText(""" + def x(y): + for i in range(10): + if i > 10: + return i + pass + """) + + fun testForLoopWithElsePrint() = doTestByText(""" + def x(y): + if y: + return 1 + for i in range(10): + if i > 10: + break + else: + print() + """) + + // -------------------------- + // Assert Tests + // -------------------------- + + fun testAssertTrue() = doTestByText(""" + def f(x): + if x: + return 1 + assert True + """) + + fun testAssertUndecidable() = doTestByText(""" + def f(x): + if x: + return 1 + assert x + """) + + fun testAssertFalseNoWarning() = doTestByText(""" + def f(x): + if x: + return 1 + assert False + """) + + // -------------------------- + // Function Call Tests + // -------------------------- + + fun testNormalCallExpression() = doTestByText(""" + def f(x): + if x: + return 1 + foo() + """) + + fun testNoReturnCall() = doTestByText(""" + import sys + def f(x): + if x: + return 1 + sys.exit() + """) + + + // -------------------------- + // Other Statement Tests + // -------------------------- + + fun testTryExceptNoWarning() = doTestByText(""" + def f(x): + if x: + return 1 + try: + risky() + except Exception: + handle() + """) + + fun testMatchStatement() = doTestByText(""" + def f(x): + if x: + return 1 + match x: + case 1: + foo() + case _: + bar() + """) + + fun testWithStatement() = doTestByText(""" + def f(): + if x: + return 1 + with ctx(): + body() + """) + + fun testWithStatementSuppressingExceptions() = doTestByText(""" + class ctx: + def __enter__(self): ... + def __exit__(self, exc_type, exc_val, exc_tb) -> bool: ... + + def f(): + if x: + return 1 + with ctx(): + body() + """) + + fun testNestedFunctions() = doTestByText(""" + def f(x): + if x: + return 1 + def g(): + return 2 + """) + + fun testNestedClass() = doTestByText(""" + def f(x): + if x: + return 1 + class C: + x = 5 + """) + + + fun testNonexistentFunctionCall() = doTestByText(""" + def func_unknown(x): + if x > 0: + return False + no_such_function() +""") + + fun testNoReturnFunction() = doTestByText(""" + def func_no_noreturn(x): + if x > 0: + return False + print("", end="") +""") + + fun testMatchStatementInconsistentReturns() = doTestByText(""" + def x(y): + match y: + case 0: + return 1 + case 1: + print() +""") + + // PY-80524 + fun testIfInsideTryExcept() = doTestByText(""" + def fn(): + try: + if some_condition(): + return True + # no warning should be highlighted here + some_non_returning_function() + except Exception: + return False + """) + +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnExplicitFixTest.kt b/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnExplicitFixTest.kt new file mode 100644 index 000000000000..56d9beec337d --- /dev/null +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnExplicitFixTest.kt @@ -0,0 +1,31 @@ +// Copyright 2000-2025 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.PyInconsistentReturnsInspection + +@TestDataPath("\$CONTENT_ROOT/../testData/quickFixes/PyMakeReturnExplicitFixTest") +class PyMakeReturnExplicitFixTest : PyQuickFixTestCase() { + fun testAddExplicitReturn() { + doQuickFixTest(PyPsiBundle.message("QFIX.add.explicit.return.none")) + } + + fun testReplaceWithReturnNone() { + doQuickFixTest(PyPsiBundle.message("QFIX.replace.with.return.none")) + } + + + // Copied from PyQuickFixTestCase to enable weak warnings + override fun doQuickFixTest(hint: String) { + val testFileName = getTestName(true) + myFixture.enableInspections(PyInconsistentReturnsInspection::class.java) + myFixture.configureByFile("$testFileName.py") + myFixture.checkHighlighting(false, false, true) + val intentionAction = myFixture.findSingleIntention(hint) + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile(testFileName + "_after.py", true) + } +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnsExplicitFixTest.java b/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnsExplicitFixTest.java deleted file mode 100644 index d4c59aaeb690..000000000000 --- a/python/testSrc/com/jetbrains/python/quickFixes/PyMakeReturnsExplicitFixTest.java +++ /dev/null @@ -1,37 +0,0 @@ -// 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.PyInconsistentReturnsInspection; - -@TestDataPath("$CONTENT_ROOT/../testData/quickFixes/PyMakeReturnsExplicitFixTest/") -public class PyMakeReturnsExplicitFixTest extends PyQuickFixTestCase { - - public void testAddReturnsFromReturnStmt() { - doQuickFixTest(PyInconsistentReturnsInspection.class, PyPsiBundle.message("QFIX.NAME.make.return.stmts.explicit")); - } - - // PY-80493 - public void testContextManagerSuppressingException() { - doQuickFixTest(PyInconsistentReturnsInspection.class, PyPsiBundle.message("QFIX.NAME.make.return.stmts.explicit")); - } - - // PY-80493 - public void testContextManagerNotSuppressingException() { - doQuickFixTest(PyInconsistentReturnsInspection.class, PyPsiBundle.message("QFIX.NAME.make.return.stmts.explicit")); - } - - @Override - protected void doQuickFixTest(final Class inspectionClass, final String hint) { - final String testFileName = getTestName(true); - myFixture.enableInspections(inspectionClass); - myFixture.configureByFile(testFileName + ".py"); - myFixture.checkHighlighting(true, false, true); - final var intentionAction = myFixture.findSingleIntention(hint); - assertNotNull(intentionAction); - myFixture.launchAction(intentionAction); - myFixture.checkResultByFile(testFileName + "_after.py", true); - } -}