diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java index 8b6813b47095..edc1832973ef 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java @@ -29,11 +29,12 @@ import com.intellij.psi.util.QualifiedName; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.psi.*; -import com.jetbrains.python.psi.impl.ParamHelper; -import com.jetbrains.python.psi.impl.PyAugAssignmentStatementNavigator; -import com.jetbrains.python.psi.impl.PyEvaluator; -import com.jetbrains.python.psi.impl.PyImportStatementNavigator; +import com.jetbrains.python.psi.impl.*; +import com.jetbrains.python.psi.resolve.PyResolveUtil; +import com.jetbrains.python.psi.types.TypeEvalContext; import kotlin.Triple; import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; @@ -46,8 +47,6 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { @NotNull private static final Set EXCEPTION_SUPPRESSORS = ImmutableSet.of("suppress", "assertRaises", "assertRaisesRegex"); - private static final Set KNOWN_NORETURNS = ImmutableSet.of("sys.exit", "exit", "pytest.fail"); - private final ControlFlowBuilder myBuilder = new ControlFlowBuilder(); public ControlFlow buildControlFlow(@NotNull final ScopeOwner owner) { @@ -135,7 +134,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { public void visitPyCallExpression(final @NotNull PyCallExpression node) { final PyExpression callee = node.getCallee(); // Flow abrupted - if (callee != null && assumeDeadEnd(callee)) { + if (callee != null && isCallOfNoReturnFunction(callee)) { callee.accept(this); for (PyExpression expression : node.getArguments()) { expression.accept(this); @@ -227,7 +226,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { public void visitPyDelStatement(@NotNull PyDelStatement node) { myBuilder.startNode(node); for (PyExpression target : node.getTargets()) { - if (target instanceof PyReferenceExpression expr){ + if (target instanceof PyReferenceExpression expr) { myBuilder.addNode(ReadWriteInstruction.newInstruction(myBuilder, target, expr.getName(), ReadWriteInstruction.ACCESS.DELETE)); PyExpression qualifier = expr.getQualifier(); if (qualifier != null) { @@ -993,23 +992,29 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { if (target != null) target.accept(this); } - private static boolean assumeDeadEnd(final @NotNull PyExpression callee) { - String repr = PyUtil.getReadableRepr(callee, true); - if (KNOWN_NORETURNS.contains(repr)) { - return true; - } - /* Since we can't fully resolve the call during the building of the control flow graph, - * here we make an assumption that the class which contains self.fail() call is the real - * test class and self.fail() is actually unittest.TestCase.fail() call which leads to flow abruption (see PY-23859). - * This approach does not completely eliminate false positives, but it helps to reduce their number. */ - if (repr.equals("self.fail")) { - PyClass clazz = PsiTreeUtil.getParentOfType(callee, PyClass.class); - if (clazz != null && clazz.getName() != null) { - String className = clazz.getName(); - boolean classNameContainsTest = className.contains("Test"); - if (classNameContainsTest) { + private static boolean isCallOfNoReturnFunction(@NotNull PyExpression callee) { + if (callee instanceof PyReferenceExpression expression) { + QualifiedName qName = expression.asQualifiedName(); + + if (qName == null) { + return false; + } + + ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(expression); + + // Flow-insensitive context is required to prevent recursive control flow access during the resolve process + TypeEvalContext context = TypeEvalContext.codeInsightFallback(callee.getProject()); + + while (scopeOwner != null) { + boolean resolvesToNoReturnOrNever = StreamEx + .of(PyResolveUtil.resolveQualifiedNameInScope(qName, scopeOwner, context)) + .select(PyFunction.class) + .anyMatch(function -> PyTypingTypeProvider.isNoReturn(function, context)); + + if (resolvesToNoReturnOrNever) { return true; } + scopeOwner = ScopeUtil.getScopeOwner(scopeOwner); } } return false; diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index 225ed2daf748..e93af74164f0 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -43,7 +43,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -91,6 +90,9 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext classVarType = getClassVarType(resolved, context); + final Ref classVarType = unwrapTypeModifier(resolved, context, CLASS_VAR); if (classVarType != null) { return classVarType; } @@ -804,7 +806,7 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext finalType = getFinalType(resolved, context); + final Ref finalType = unwrapTypeModifier(resolved, context, FINAL, FINAL_EXT); if (finalType != null) { return finalType; } @@ -932,19 +934,6 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext getClassVarType(@NotNull PsiElement resolved, @NotNull Context context) { - if (resolved instanceof PySubscriptionExpression subscriptionExpr) { - if (resolvesToClassVar(subscriptionExpr.getOperand(), context.getTypeContext())) { - final PyExpression indexExpr = subscriptionExpr.getIndexExpression(); - if (indexExpr != null) { - return getType(indexExpr, context); - } - } - } - return null; - } - @Nullable private static Ref getAliasedType(@NotNull PsiElement resolved, @NotNull Context context) { if (resolved instanceof PyReferenceExpression && ((PyReferenceExpression)resolved).asQualifiedName() != null) { @@ -1119,9 +1108,9 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext getFinalType(@NotNull PsiElement resolved, @NotNull Context context) { + private static Ref unwrapTypeModifier(@NotNull PsiElement resolved, @NotNull Context context, String... type) { if (resolved instanceof PySubscriptionExpression subscriptionExpr) { - if (resolvesToFinal(subscriptionExpr.getOperand(), context.getTypeContext())) { + if (resolvesToQualifiedNames(subscriptionExpr.getOperand(), context.getTypeContext(), type)) { final PyExpression indexExpr = subscriptionExpr.getIndexExpression(); if (indexExpr != null) { return getType(indexExpr, context); @@ -1132,24 +1121,24 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext boolean isSpecialModifierImpl(@NotNull T owner, - @NotNull TypeEvalContext context, - @NotNull BiFunction resolver) { + private static boolean typeHintedWithName(@NotNull T owner, + @NotNull TypeEvalContext context, + String... names) { final PyExpression annotation = getAnnotationValue(owner, context); if (annotation instanceof PySubscriptionExpression) { - return resolver.apply(((PySubscriptionExpression)annotation).getOperand(), context); + return resolvesToQualifiedNames(((PySubscriptionExpression)annotation).getOperand(), context, names); } else if (annotation instanceof PyReferenceExpression) { - return resolver.apply(annotation, context); + return resolvesToQualifiedNames(annotation, context, names); } final String typeCommentValue = owner.getTypeCommentAnnotation(); final PyExpression typeComment = typeCommentValue == null ? null : toExpression(typeCommentValue, owner); if (typeComment instanceof PySubscriptionExpression) { - return resolver.apply(((PySubscriptionExpression)typeComment).getOperand(), context); + return resolvesToQualifiedNames(((PySubscriptionExpression)typeComment).getOperand(), context, names); } else if (typeComment instanceof PyReferenceExpression) { - return resolver.apply(typeComment, context); + return resolvesToQualifiedNames(typeComment, context, names); } return false; @@ -1161,25 +1150,23 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext boolean isFinal(@NotNull T owner, @NotNull TypeEvalContext context) { - return PyUtil.getParameterizedCachedValue(owner, context, p -> isSpecialModifierImpl(owner, p, (e, c) -> { - return resolvesToFinal(e, c); - })); - } - - private static boolean resolvesToFinal(@NotNull PyExpression expression, @NotNull TypeEvalContext context) { - final var qualifiedNames = resolveToQualifiedNames(expression, context); - return qualifiedNames.contains(FINAL) || qualifiedNames.contains(FINAL_EXT); + return PyUtil.getParameterizedCachedValue(owner, context, p -> + typeHintedWithName(owner, context, FINAL, FINAL_EXT)); } public static boolean isClassVar(@NotNull T owner, @NotNull TypeEvalContext context) { - return PyUtil.getParameterizedCachedValue(owner, context, p -> isSpecialModifierImpl(owner, p, (e, c) -> { - return resolvesToClassVar(e, c); - })); + return PyUtil.getParameterizedCachedValue(owner, context, p -> + typeHintedWithName(owner, context, CLASS_VAR)); } - private static boolean resolvesToClassVar(@NotNull PyExpression expression, @NotNull TypeEvalContext context) { + public static boolean isNoReturn(@NotNull PyFunction function, @NotNull TypeEvalContext context) { + return PyUtil.getParameterizedCachedValue(function, context, p -> + typeHintedWithName(function, context, NO_RETURN, NO_RETURN_EXT, NEVER, NEVER_EXT)); + } + + private static boolean resolvesToQualifiedNames(@NotNull PyExpression expression, @NotNull TypeEvalContext context, String... names) { final var qualifiedNames = resolveToQualifiedNames(expression, context); - return qualifiedNames.contains(CLASS_VAR); + return ContainerUtil.exists(names, qualifiedNames::contains); } @Nullable 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 1c399aafcae4..cac4163a6e5b 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 @@ -219,7 +219,8 @@ public class PyTypeCheckerInspection extends PyInspection { } } - if (PyUtil.isInitMethod(node) && !(getExpectedReturnType(node) instanceof PyNoneType)) { + if (PyUtil.isInitMethod(node) && !(getExpectedReturnType(node) instanceof PyNoneType + || PyTypingTypeProvider.isNoReturn(node, myTypeEvalContext))) { registerProblem(annotation != null ? annotation.getValue() : node.getTypeComment(), PyPsiBundle.message("INSP.type.checker.init.should.return.none")); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java index d427e0372b5e..a032af314168 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java @@ -644,19 +644,19 @@ public final class PyUtil { * @param

key type */ @NotNull - public static T getParameterizedCachedValue(@NotNull PsiElement element, @Nullable P param, @NotNull NotNullFunction f) { + public static T getParameterizedCachedValue(@NotNull PsiElement element, @Nullable P param, @NotNull Function f) { final T result = getNullableParameterizedCachedValue(element, param, f); assert result != null; return result; } /** - * Same as {@link #getParameterizedCachedValue(PsiElement, Object, NotNullFunction)} but allows nulls. + * Same as {@link #getParameterizedCachedValue(PsiElement, Object, Function)} but allows nulls. */ @Nullable public static T getNullableParameterizedCachedValue(@NotNull PsiElement element, @Nullable P param, - @NotNull NullableFunction f) { + @NotNull Function f) { final CachedValuesManager manager = CachedValuesManager.getManager(element.getProject()); final Map, Optional> cache = CachedValuesManager.getCachedValue(element, manager.getKeyForClass(f.getClass()), () -> { // concurrent hash map is a null-hostile collection diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.py b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.py deleted file mode 100644 index c422bed062d9..000000000000 --- a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.py +++ /dev/null @@ -1,6 +0,0 @@ -def test_fail(): - if True == False: - pytest.fail() - print("should be reported as unreachable") - else: - return 1 \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.txt b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.txt deleted file mode 100644 index 0b964064632e..000000000000 --- a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.txt +++ /dev/null @@ -1,11 +0,0 @@ -0(1) element: null -1(2) element: PyIfStatement -2(3) READ ACCESS: True -3(4,8) READ ACCESS: False -4(5) element: PyStatementList. Condition: True == False:true -5(6) element: PyExpressionStatement -6(10) READ ACCESS: pytest -7(10) element: PyPrintStatement -8(9) element: PyStatementList. Condition: True == False:false -9(10) element: PyReturnStatement -10() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.txt b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.txt deleted file mode 100644 index a603b2f5abd3..000000000000 --- a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.txt +++ /dev/null @@ -1,17 +0,0 @@ -0(1) element: null -1(2) element: PyTryExceptStatement -2(3,8) element: PyTryPart -3(4,8) element: PyAssignmentStatement -4(5,8) READ ACCESS: int -5(6,8) element: PySubscriptionExpression -6(7,8) READ ACCESS: sys -7(8,13) WRITE ACCESS: n -8(9) element: PyExceptPart -9(10) READ ACCESS: ValueError -10(11) element: PyPrintStatement -11(12) element: PyExpressionStatement -12(16) READ ACCESS: sys -13(14) element: PyPrintStatement -14(15) READ ACCESS: str -15(16) READ ACCESS: n -16() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.py b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterExit.py similarity index 100% rename from python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.py rename to python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterExit.py diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.txt b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterExit.txt similarity index 100% rename from python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.txt rename to python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterExit.txt diff --git a/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNever.py b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNever.py new file mode 100644 index 000000000000..797129fc8cbb --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNever.py @@ -0,0 +1,7 @@ +from typing import Never + +def stop() -> Never: + raise RuntimeError('no way') + +stop() +print("ureachable") \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNever.txt b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNever.txt new file mode 100644 index 000000000000..da3960f947d8 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNever.txt @@ -0,0 +1,10 @@ +0(1) element: null +1(2) element: PyFromImportStatement +2(3) WRITE ACCESS: Never +3(4) element: PyFunction('stop') +4(5) READ ACCESS: Never +5(6) WRITE ACCESS: stop +6(7) element: PyExpressionStatement +7(9) READ ACCESS: stop +8(9) element: PyPrintStatement +9() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNoReturn.py b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNoReturn.py new file mode 100644 index 000000000000..bd19ae3d6581 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNoReturn.py @@ -0,0 +1,7 @@ + from typing import NoReturn + + def stop() -> NoReturn: + raise RuntimeError('no way') + +stop() +print("ureachable") \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNoReturn.txt b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNoReturn.txt new file mode 100644 index 000000000000..6019b85ca18a --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterNoReturn.txt @@ -0,0 +1,10 @@ +0(1) element: null +1(2) element: PyFromImportStatement +2(3) WRITE ACCESS: NoReturn +3(4) element: PyFunction('stop') +4(5) READ ACCESS: NoReturn +5(6) WRITE ACCESS: stop +6(7) element: PyExpressionStatement +7(9) READ ACCESS: stop +8(9) element: PyPrintStatement +9() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.py b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSelfFail.py similarity index 100% rename from python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.py rename to python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSelfFail.py diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.txt b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSelfFail.txt similarity index 100% rename from python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.txt rename to python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSelfFail.txt diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.py b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSysExit.py similarity index 92% rename from python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.py rename to python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSysExit.py index 3001b6758cf4..54af4baa2573 100644 --- a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.py +++ b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSysExit.py @@ -1,3 +1,5 @@ +import sys + try: n = int(sys.argv[1]) except ValueError: diff --git a/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSysExit.txt b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSysExit.txt new file mode 100644 index 000000000000..c89ec60426c0 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowIsAbruptAfterSysExit.txt @@ -0,0 +1,19 @@ +0(1) element: null +1(2) element: PyImportStatement +2(3) WRITE ACCESS: sys +3(4) element: PyTryExceptStatement +4(5,10) element: PyTryPart +5(6,10) element: PyAssignmentStatement +6(7,10) READ ACCESS: int +7(8,10) element: PySubscriptionExpression +8(9,10) READ ACCESS: sys +9(10,15) WRITE ACCESS: n +10(11) element: PyExceptPart +11(12) READ ACCESS: ValueError +12(13) element: PyPrintStatement +13(14) element: PyExpressionStatement +14(18) READ ACCESS: sys +15(16) element: PyPrintStatement +16(17) READ ACCESS: str +17(18) READ ACCESS: n +18() element: null \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/CodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName.py b/python/testData/inspections/PyUnreachableCodeInspection/CodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName.py deleted file mode 100644 index dd617970ff83..000000000000 --- a/python/testData/inspections/PyUnreachableCodeInspection/CodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName.py +++ /dev/null @@ -1,8 +0,0 @@ -# PY-23859 - -from unittest import TestCase - -class C(TestCase): - def test_1(self): - self.fail() - return -42 \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterImportedNoReturnFunction/a.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterImportedNoReturnFunction/a.py new file mode 100644 index 000000000000..e45ab525604f --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterImportedNoReturnFunction/a.py @@ -0,0 +1,5 @@ +from util import panic + +def foo(): + panic("Error!") + print("Should be reported as unreachable") \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterImportedNoReturnFunction/util.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterImportedNoReturnFunction/util.py new file mode 100644 index 000000000000..f144e6ebb048 --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterImportedNoReturnFunction/util.py @@ -0,0 +1,5 @@ +from typing import NoReturn + +def panic(m) -> NoReturn: + print(f'Help: {m}') + raise SystemExit \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNever.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNever.py new file mode 100644 index 000000000000..2ecf5a51106a --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNever.py @@ -0,0 +1,7 @@ +from typing import Never + +def stop() -> Never: + raise RuntimeError('no way') + +stop() +print("Should be reported as unreachable") \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNoReturnFunction.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNoReturnFunction.py new file mode 100644 index 000000000000..f7c3afca187b --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNoReturnFunction.py @@ -0,0 +1,7 @@ +from typing import NoReturn + +def stop() -> NoReturn: + raise RuntimeError('no way') + +stop() +print("Should be reported as unreachable") \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterPytestFail.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterPytestFail.py deleted file mode 100644 index 5994a2796cc1..000000000000 --- a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterPytestFail.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest - -def test_fail(): - if True == False: - pytest.fail() - print("should be reported as unreachable") - else: - return 1 \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterSelfFailInClassContainingTestInName.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterSelfFail.py similarity index 100% rename from python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterSelfFailInClassContainingTestInName.py rename to python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterSelfFail.py diff --git a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java index 5e3747c00308..02f577728e88 100644 --- a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java +++ b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java @@ -479,17 +479,17 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase { } // PY-7758 - public void testControlFlowAbruptedOnExit() { + public void testControlFlowIsAbruptAfterExit() { doTest(); } // PY-7758 - public void testControlFlowAbruptedOnSysExit() { + public void testControlFlowIsAbruptAfterSysExit() { doTest(); } // PY-23859 - public void testControlFlowAbruptedOnRealSelfFailAssumedByClassName() { + public void testControlFlowIsAbruptAfterSelfFail() { final String testName = getTestName(false); configureByFile(testName + ".py"); final String fullPath = getTestDataPath() + testName + ".txt"; @@ -498,10 +498,17 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase { check(fullPath, flow); } - public void testControlFlowAbruptedOnPytestFail() { - doTestFirstStatement(); + // PY-24273 + public void testControlFlowIsAbruptAfterNoReturn() { + doTest(); } + // TODO migrate this test class to Python 3 SDK by default to make this test work + // PY-53703 + //public void testControlFlowIsAbruptAfterNever() { + // doTest(); + //} + private void doTestFirstStatement() { final String testName = getTestName(false); configureByFile(testName + ".py"); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index b3f733cd6404..a013b039c676 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -1516,4 +1516,19 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase { group: GroupWithOtherKey[str, int] = {"key": 1, "group": [], "some_other_key": ''}""") ); } + + // PY-27551 + public void testDunderInitAnnotatedWithNoReturn() { + runWithLanguageLevel( + LanguageLevel.getLatest(), + () -> doTestByText(""" + from typing import NoReturn + + class Test: + def __init__(self) -> NoReturn: + raise Exception() + + """) + ); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java index 8f877f50ab07..6d948d471d03 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java @@ -219,17 +219,25 @@ public class PyUnreachableCodeInspectionTest extends PyInspectionTestCase { } // PY-23859 - public void testUnreachableCodeReportedAfterSelfFailInClassContainingTestInName() { + public void testUnreachableCodeReportedAfterSelfFail() { doTest(); } - // PY-23859 - public void testCodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName() { + // PY-24273 + public void testUnreachableCodeReportedAfterNoReturnFunction() { doTest(); } - public void testUnreachableCodeReportedAfterPytestFail() { - doTest(); + // PY-24273 + public void testUnreachableCodeReportedAfterImportedNoReturnFunction() { + doMultiFileTest(); + } + + // PY-53703 + public void testUnreachableCodeReportedAfterNever() { + runWithLanguageLevel(LanguageLevel.getLatest(), () -> { + doTest(); + }); } @NotNull