mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-05 01:50:56 +07:00
PY-80524 Explicit return statement expected false positive for an if inside try / except
(cherry picked from commit 6877fc7b34622d7ae884233af96e06ea92c918c1) IJ-MR-161824 GitOrigin-RevId: 8c3219ea8bb8f547f7c314af8cb0d432c042cd7c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a8934362ff
commit
d8f57b77c6
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -468,7 +468,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
|
||||
@@ -1022,8 +1023,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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<PyIfStatement>()?.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)
|
||||
}
|
||||
@@ -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<PyStatement>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PyFunction> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PyWithItem> {
|
||||
return childrenToPsi(PyAstWithStatement.WITH_ITEM, PyWithItem.EMPTY_ARRAY)
|
||||
}
|
||||
|
||||
@Override
|
||||
public PyWithItem[] getWithItems() {
|
||||
return childrenToPsi(WITH_ITEM, PyWithItem.EMPTY_ARRAY);
|
||||
override fun getComponents(): List<PyWithItem> {
|
||||
return withItems.toList()
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<? extends PsiElement> getComponents() {
|
||||
return Arrays.asList(getWithItems());
|
||||
override fun isSuppressingExceptions(context: TypeEvalContext): Boolean {
|
||||
return withItems.any { it.isSuppressingExceptions(context) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
def func(x, y, z):
|
||||
<weak_warning descr="Missing return statement on some paths"><caret>if x:</weak_warning>
|
||||
return 42
|
||||
elif y:
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths">return</weak_warning>
|
||||
@@ -0,0 +1,6 @@
|
||||
def func(x, y, z):
|
||||
if x:
|
||||
return 42
|
||||
elif y:
|
||||
return
|
||||
return None
|
||||
@@ -0,0 +1,5 @@
|
||||
def func(x, y, z):
|
||||
<weak_warning descr="Missing return statement on some paths">if x: </weak_warning>
|
||||
return 42
|
||||
elif y:
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths"><caret>return</weak_warning>
|
||||
@@ -0,0 +1,5 @@
|
||||
def func(x, y, z):
|
||||
if x:
|
||||
return 42
|
||||
elif y:
|
||||
return None
|
||||
@@ -1,15 +0,0 @@
|
||||
def f(x) -> int | None:
|
||||
y = 42
|
||||
print(y)
|
||||
<weak_warning descr="Explicit return statement expected">if x == 1:
|
||||
return 42
|
||||
elif x == 2:
|
||||
<weak_warning descr="Explicit return value expected">return<caret></weak_warning>
|
||||
elif x == 3:
|
||||
raise Exception()
|
||||
elif x == 4:
|
||||
assert False
|
||||
elif x == 5:
|
||||
<weak_warning descr="Explicit return statement expected">assert x</weak_warning>
|
||||
elif x == 4:
|
||||
<weak_warning descr="Explicit return statement expected">pass</weak_warning></weak_warning>
|
||||
@@ -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
|
||||
@@ -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()
|
||||
<weak_warning descr="Explicit return statement expected">if bool():
|
||||
return 1<caret></weak_warning>
|
||||
@@ -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
|
||||
@@ -1,10 +0,0 @@
|
||||
class SuppressingContext:
|
||||
def __enter__(self):
|
||||
...
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
||||
...
|
||||
|
||||
def foo():
|
||||
<weak_warning descr="Explicit return statement expected">with SuppressingContext() as st:
|
||||
foo()
|
||||
return 1<caret></weak_warning>
|
||||
@@ -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
|
||||
@@ -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<out PyInspection> = 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
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths">return</weak_warning>
|
||||
""")
|
||||
|
||||
fun testExplicitReturnAndImplicitStmt() = doTestByText("""
|
||||
def f(cond):
|
||||
<weak_warning descr="Missing return statement on some paths">if cond:</weak_warning>
|
||||
return 1
|
||||
""")
|
||||
|
||||
// --------------------------
|
||||
// If Tests
|
||||
// --------------------------
|
||||
|
||||
fun testIfElse() = doTestByText("""
|
||||
def f(cond, cond2, cond3):
|
||||
if cond:
|
||||
return 'yes'
|
||||
elif cond2:
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths">return</weak_warning>
|
||||
elif cond3:
|
||||
<weak_warning descr="Missing return statement on some paths">print()</weak_warning>
|
||||
else:
|
||||
return 'no'
|
||||
""")
|
||||
|
||||
fun testIfNoElse() = doTestByText("""
|
||||
def f(cond, cond2, cond3):
|
||||
<weak_warning descr="Missing return statement on some paths">if cond:</weak_warning>
|
||||
return 'yes'
|
||||
elif cond2:
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths">return</weak_warning>
|
||||
elif cond3:
|
||||
print()
|
||||
""")
|
||||
|
||||
// --------------------------
|
||||
// While Tests
|
||||
// --------------------------
|
||||
|
||||
fun testWhileTrueNoWarning() = doTestByText("""
|
||||
def f():
|
||||
while True:
|
||||
do_something()
|
||||
""")
|
||||
|
||||
fun testWhile() = doTestByText("""
|
||||
def f():
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">while not x:</weak_warning>
|
||||
break
|
||||
""")
|
||||
|
||||
fun testWhileImplicitNone() = doTestByText("""
|
||||
def f():
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">while not x:</weak_warning>
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths">return</weak_warning>
|
||||
""")
|
||||
|
||||
fun testWhileWithElse() = doTestByText("""
|
||||
def f():
|
||||
if x:
|
||||
return 1
|
||||
while not x:
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths">return</weak_warning>
|
||||
else:
|
||||
<weak_warning descr="Missing return statement on some paths">x = 5</weak_warning>
|
||||
""")
|
||||
|
||||
// --------------------------
|
||||
// For Tests
|
||||
// --------------------------
|
||||
|
||||
fun testForNoElse() = doTestByText("""
|
||||
def f(x):
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">for i in range(10):</weak_warning>
|
||||
if i > 10:
|
||||
<weak_warning descr="'return' without value is inconsistent with other paths">return</weak_warning>
|
||||
""")
|
||||
|
||||
fun testForLoopIncomplete() = doTestByText("""
|
||||
def x(y):
|
||||
for i in range(10):
|
||||
if i > 10:
|
||||
return i
|
||||
<weak_warning descr="Missing return statement on some paths">pass</weak_warning>
|
||||
""")
|
||||
|
||||
fun testForLoopWithElsePrint() = doTestByText("""
|
||||
def x(y):
|
||||
if y:
|
||||
return 1
|
||||
for i in range(10):
|
||||
if i > 10:
|
||||
break
|
||||
else:
|
||||
<weak_warning descr="Missing return statement on some paths">print()</weak_warning>
|
||||
""")
|
||||
|
||||
// --------------------------
|
||||
// Assert Tests
|
||||
// --------------------------
|
||||
|
||||
fun testAssertTrue() = doTestByText("""
|
||||
def f(x):
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">assert True</weak_warning>
|
||||
""")
|
||||
|
||||
fun testAssertUndecidable() = doTestByText("""
|
||||
def f(x):
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">assert x</weak_warning>
|
||||
""")
|
||||
|
||||
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
|
||||
<weak_warning descr="Missing return statement on some paths">foo()</weak_warning>
|
||||
""")
|
||||
|
||||
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:
|
||||
<weak_warning descr="Missing return statement on some paths">foo()</weak_warning>
|
||||
case _:
|
||||
<weak_warning descr="Missing return statement on some paths">bar()</weak_warning>
|
||||
""")
|
||||
|
||||
fun testWithStatement() = doTestByText("""
|
||||
def f():
|
||||
if x:
|
||||
return 1
|
||||
with ctx():
|
||||
<weak_warning descr="Missing return statement on some paths">body()</weak_warning>
|
||||
""")
|
||||
|
||||
fun testWithStatementSuppressingExceptions() = doTestByText("""
|
||||
class ctx:
|
||||
def __enter__(self): ...
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> bool: ...
|
||||
|
||||
def f():
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">with ctx():</weak_warning>
|
||||
body()
|
||||
""")
|
||||
|
||||
fun testNestedFunctions() = doTestByText("""
|
||||
def f(x):
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">def g():</weak_warning>
|
||||
return 2
|
||||
""")
|
||||
|
||||
fun testNestedClass() = doTestByText("""
|
||||
def f(x):
|
||||
if x:
|
||||
return 1
|
||||
<weak_warning descr="Missing return statement on some paths">class C:</weak_warning>
|
||||
x = 5
|
||||
""")
|
||||
|
||||
|
||||
fun testNonexistentFunctionCall() = doTestByText("""
|
||||
def func_unknown(x):
|
||||
if x > 0:
|
||||
return False
|
||||
<weak_warning descr="Missing return statement on some paths">no_such_function()</weak_warning>
|
||||
""")
|
||||
|
||||
fun testNoReturnFunction() = doTestByText("""
|
||||
def func_no_noreturn(x):
|
||||
if x > 0:
|
||||
return False
|
||||
<weak_warning descr="Missing return statement on some paths">print("", end="")</weak_warning>
|
||||
""")
|
||||
|
||||
fun testMatchStatementInconsistentReturns() = doTestByText("""
|
||||
def x(y):
|
||||
<weak_warning descr="Missing return statement on some paths">match y:</weak_warning>
|
||||
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
|
||||
""")
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user