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 cc8bfe489257..e920a20480ba 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 @@ -39,16 +39,15 @@ import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; 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) { @@ -136,9 +135,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { public void visitPyCallExpression(final @NotNull PyCallExpression node) { final PyExpression callee = node.getCallee(); // Flow abrupted - final String repr = PyUtil.getReadableRepr(callee, true); - if (callee != null && ("sys.exit".equals(repr) || - "self.fail".equals(repr))) { + if (callee != null && assumeDeadEnd(callee)) { callee.accept(this); for (PyExpression expression : node.getArguments()) { expression.accept(this); @@ -997,6 +994,28 @@ 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) { + return true; + } + } + } + return false; + } + private void abruptFlow(final PsiElement node) { // Here we process pending instructions!!! myBuilder.processPending((pendingScope, instruction) -> { diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.py b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.py new file mode 100644 index 000000000000..c18cc6ceab64 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.py @@ -0,0 +1,7 @@ +try: + n = int(sys.argv[1]) +except ValueError: + print("both arguments should be numbers") + exit() + +print("Please, input " + str(n) + " file names") \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.txt b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.txt new file mode 100644 index 000000000000..c3c39f5f54e4 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnExit.txt @@ -0,0 +1,17 @@ +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: exit +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/ControlFlowAbruptedOnPytestFail.py b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.py new file mode 100644 index 000000000000..c422bed062d9 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.py @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000000..0b964064632e --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnPytestFail.txt @@ -0,0 +1,11 @@ +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/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.py b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.py new file mode 100644 index 000000000000..6c738e231f1b --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.py @@ -0,0 +1,10 @@ +import unittest + +class TestStringMethods(unittest.TestCase): + + def test_is_ok(self): + str = get_response() + if isinstance(str, int): + self.fail() + print("Not a string!") + self.assert_(str, "OK") \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.txt b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.txt new file mode 100644 index 000000000000..a17cd18550b0 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnRealSelfFailAssumedByClassName.txt @@ -0,0 +1,19 @@ +0(1) element: null +1(2) WRITE ACCESS: self +2(3) element: PyAssignmentStatement +3(4) READ ACCESS: get_response +4(5) WRITE ACCESS: str +5(6) element: PyIfStatement +6(7) READ ACCESS: isinstance +7(8) READ ACCESS: str +8(9,14) READ ACCESS: int +9(10) element: PyStatementList. Condition: isinstance(str, int):true +10(11) ASSERTTYPE ACCESS: str +11(12) element: PyExpressionStatement +12(18) READ ACCESS: self +13(15) element: PyPrintStatement +14(15) ASSERTTYPE ACCESS: str +15(16) element: PyExpressionStatement +16(17) READ ACCESS: self +17(18) READ ACCESS: str +18() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.py b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.py new file mode 100644 index 000000000000..3001b6758cf4 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.py @@ -0,0 +1,7 @@ +try: + n = int(sys.argv[1]) +except ValueError: + print("both arguments should be numbers") + sys.exit() + +print("Please, input " + str(n) + " file names") \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.txt b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.txt new file mode 100644 index 000000000000..a603b2f5abd3 --- /dev/null +++ b/python/testData/codeInsight/controlflow/ControlFlowAbruptedOnSysExit.txt @@ -0,0 +1,17 @@ +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/inspections/PyUnboundLocalVariableInspection/VariableNotReportedAfterBuiltinExit.py b/python/testData/inspections/PyUnboundLocalVariableInspection/VariableNotReportedAfterBuiltinExit.py new file mode 100644 index 000000000000..0eb7d29cc002 --- /dev/null +++ b/python/testData/inspections/PyUnboundLocalVariableInspection/VariableNotReportedAfterBuiltinExit.py @@ -0,0 +1,7 @@ +try: + n = int(sys.argv[1]) +except: + print("both arguments should be numbers") + exit() + +print("Please, input " + str(n) + " file names") \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/CodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName.py b/python/testData/inspections/PyUnreachableCodeInspection/CodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName.py new file mode 100644 index 000000000000..dd617970ff83 --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/CodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName.py @@ -0,0 +1,8 @@ +# 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/Unreachable.py b/python/testData/inspections/PyUnreachableCodeInspection/Unreachable.py index a5d3d11b04f7..20120935cb7e 100644 --- a/python/testData/inspections/PyUnreachableCodeInspection/Unreachable.py +++ b/python/testData/inspections/PyUnreachableCodeInspection/Unreachable.py @@ -68,15 +68,6 @@ def f(): return f, foo -# PY-3886 -def f(): - from unittest import TestCase - class C(TestCase): - def test_1(self): - self.fail() - return -42 - - # PY-4149 def f(): try: diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterBuiltinExit.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterBuiltinExit.py new file mode 100644 index 000000000000..1caf122724d5 --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterBuiltinExit.py @@ -0,0 +1,4 @@ +def test_exit(): + exit() + print("should be reported as unreachable") + return True \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterPytestFail.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterPytestFail.py new file mode 100644 index 000000000000..5994a2796cc1 --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterPytestFail.py @@ -0,0 +1,8 @@ +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/UnreachableCodeReportedAfterSelfFailInClassContainingTestInName.py new file mode 100644 index 000000000000..6c70ebca34d1 --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterSelfFailInClassContainingTestInName.py @@ -0,0 +1,13 @@ +# PY-23859, PY-3886 + +from unittest import TestCase + +class TestSomething(TestCase): + def test_1(self): + self.fail() + return -42 + +class SomethingTest(TestCase): + def test_1(self): + self.fail() + return -42 \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java index 4f27ae9aa11d..5e3747c00308 100644 --- a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java +++ b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java @@ -478,6 +478,30 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase { doTest(); } + // PY-7758 + public void testControlFlowAbruptedOnExit() { + doTest(); + } + + // PY-7758 + public void testControlFlowAbruptedOnSysExit() { + doTest(); + } + + // PY-23859 + public void testControlFlowAbruptedOnRealSelfFailAssumedByClassName() { + final String testName = getTestName(false); + configureByFile(testName + ".py"); + final String fullPath = getTestDataPath() + testName + ".txt"; + final PyClass pyClass = ((PyFile)myFile).getTopLevelClasses().get(0); + final ControlFlow flow = ControlFlowCache.getControlFlow(pyClass.getMethods()[0]); + check(fullPath, flow); + } + + public void testControlFlowAbruptedOnPytestFail() { + doTestFirstStatement(); + } + private void doTestFirstStatement() { final String testName = getTestName(false); configureByFile(testName + ".py"); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java index 918e9f5df6c8..f388ded95345 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java @@ -382,6 +382,11 @@ public class PyUnboundLocalVariableInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-7758 + public void testVariableNotReportedAfterBuiltinExit() { + doTest(); + } + @NotNull @Override protected Class getInspectionClass() { diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java index 61a883551c0c..d18d568afb41 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java @@ -216,6 +216,25 @@ public class PyUnreachableCodeInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-7758 + public void testUnreachableCodeReportedAfterBuiltinExit() { + doTest(); + } + + // PY-23859 + public void testUnreachableCodeReportedAfterSelfFailInClassContainingTestInName() { + doTest(); + } + + // PY-23859 + public void testCodeNotReportedAsUnreachableAfterSelfFailInClassNotContainingTestInName() { + doTest(); + } + + public void testUnreachableCodeReportedAfterPytestFail() { + doTest(); + } + @NotNull @Override protected Class getInspectionClass() {