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 edc1832973ef..17a40d53574c 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 @@ -48,6 +48,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { private static final Set EXCEPTION_SUPPRESSORS = ImmutableSet.of("suppress", "assertRaises", "assertRaisesRegex"); private final ControlFlowBuilder myBuilder = new ControlFlowBuilder(); + private final Map expressionToGuards = new HashMap<>(); public ControlFlow buildControlFlow(@NotNull final ScopeOwner owner) { return myBuilder.build(this, owner); @@ -58,6 +59,31 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { return this.myBuilder; } + + private void startConditionalNodeAndCheckGuards(@NotNull PsiElement element, @Nullable PyExpression condition, boolean result) { + myBuilder.startConditionalNode(element, condition, result); + addTypeGuardAssertions(condition, result); + } + + private void addTypeGuardAssertions(@Nullable PyExpression condition, boolean result) { + final PyExpression actualExpression; + final boolean negation; + if (condition instanceof PyPrefixExpression prefixExpression && prefixExpression.getOperator() == PyTokenTypes.NOT_KEYWORD) { + actualExpression = prefixExpression.getOperand(); + negation = true; + } + else { + actualExpression = condition; + negation = false; + } + var function = expressionToGuards.get(actualExpression); + if ((negation && !result || !negation && result) && function != null && actualExpression instanceof PyCallExpression callExpression) { + final var evaluator = new PyTypeAssertionEvaluator(); + evaluator.handleTypeGuardCall(callExpression, function); + InstructionBuilder.addAssertInstructions(myBuilder, evaluator); + } + } + @Override public void visitPyFunction(final @NotNull PyFunction node) { // Create node and stop here @@ -133,17 +159,24 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { @Override public void visitPyCallExpression(final @NotNull PyCallExpression node) { final PyExpression callee = node.getCallee(); + final var callNodeType = getCalleeNodeType(callee); + // Flow abrupted - if (callee != null && isCallOfNoReturnFunction(callee)) { + if (callNodeType instanceof NoReturnCallKind) { callee.accept(this); for (PyExpression expression : node.getArguments()) { expression.accept(this); } abruptFlow(node); } + else if (callNodeType instanceof TypeGuardCallKind typeGuardCallKind && node.getArguments().length > 0) { + expressionToGuards.put(node, typeGuardCallKind.pyFunction); + super.visitPyCallExpression(node); + } else { super.visitPyCallExpression(node); } + if (node.isCalleeText(PyNames.ASSERT_IS_INSTANCE)) { final PyTypeAssertionEvaluator assertionEvaluator = new PyTypeAssertionEvaluator(); node.accept(assertionEvaluator); @@ -372,7 +405,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { } myBuilder.prevInstruction = null; - myBuilder.startConditionalNode(part, lastCondition, false); + startConditionalNodeAndCheckGuards(part, lastCondition, false); } final Triple>, Boolean> currentPartResults = visitPyConditionalPart(part, node); @@ -398,7 +431,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { final PyStatementList statements = elseBranch.getStatementList(); - myBuilder.startConditionalNode(statements, lastCondition, false); + startConditionalNodeAndCheckGuards(statements, lastCondition, false); InstructionBuilder.addAssertInstructions(myBuilder, negativeAssertionEvaluator); statements.accept(this); @@ -463,7 +496,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { @NotNull PyStatement node) { final PyStatementList statements = part.getStatementList(); - myBuilder.startConditionalNode(statements, part.getCondition(), true); + startConditionalNodeAndCheckGuards(statements, part.getCondition(), true); InstructionBuilder.addAssertInstructions(myBuilder, assertionEvaluator); statements.accept(this); @@ -534,10 +567,12 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { final var outside = new ConditionalInstructionImpl(myBuilder, null, subExpression, !conditionResultToContinue); myBuilder.addNode(outside); - myBuilder.addPendingEdge(node, outside); + addTypeGuardAssertions(subExpression, !conditionResultToContinue); + myBuilder.addPendingEdge(node, myBuilder.prevInstruction); myBuilder.prevInstruction = branchingPoint; final var toTheNext = new ConditionalInstructionImpl(myBuilder, null, subExpression, conditionResultToContinue); + addTypeGuardAssertions(subExpression, conditionResultToContinue); myBuilder.addNode(toTheNext); } @@ -568,6 +603,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { final var elsePartInstruction = new ConditionalInstructionImpl(myBuilder, elsePart, mainPartResults.getFirst(), false); myBuilder.prevInstruction = null; myBuilder.addNode(elsePartInstruction); + addTypeGuardAssertions(mainPartResults.getFirst(), false); if (!isStaticallyTrue) { for (Pair pair : branchingPoints) { @@ -874,7 +910,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { final PyExpression iteratedList = c.getIteratedList(); final PyExpression iteratorVariable = c.getIteratorVariable(); if (prevCondition != null) { - myBuilder.startConditionalNode(iteratedList, prevCondition, true); + startConditionalNodeAndCheckGuards(iteratedList, prevCondition, true); prevCondition = null; } else { @@ -901,7 +937,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { continue; } if (prevCondition != null) { - myBuilder.startConditionalNode(condition, prevCondition, true); + startConditionalNodeAndCheckGuards(condition, prevCondition, true); } else { myBuilder.startNode(condition); @@ -925,7 +961,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { final PyExpression result = node.getResultExpression(); if (result != null) { if (prevCondition != null) { - myBuilder.startConditionalNode(result, prevCondition, true); + startConditionalNodeAndCheckGuards(result, prevCondition, true); } else { myBuilder.startNode(result); @@ -992,12 +1028,13 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { if (target != null) target.accept(this); } - private static boolean isCallOfNoReturnFunction(@NotNull PyExpression callee) { + @Nullable + private static CallTypeKind getCalleeNodeType(@Nullable PyExpression callee) { if (callee instanceof PyReferenceExpression expression) { QualifiedName qName = expression.asQualifiedName(); if (qName == null) { - return false; + return null; } ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(expression); @@ -1006,18 +1043,23 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { TypeEvalContext context = TypeEvalContext.codeInsightFallback(callee.getProject()); while (scopeOwner != null) { - boolean resolvesToNoReturnOrNever = StreamEx + final var result = StreamEx .of(PyResolveUtil.resolveQualifiedNameInScope(qName, scopeOwner, context)) .select(PyFunction.class) - .anyMatch(function -> PyTypingTypeProvider.isNoReturn(function, context)); - - if (resolvesToNoReturnOrNever) { - return true; - } + .map(function -> { + if (PyTypingTypeProvider.isNoReturn(function, context)) { + return NoReturnCallKind.INSTANCE; + } + if (PyTypingTypeProvider.isTypeGuard(function, context)) { + return new TypeGuardCallKind(function); + } + return null; + }).findFirst( it -> it != null); + if (result.isPresent()) return result.get(); scopeOwner = ScopeUtil.getScopeOwner(scopeOwner); } } - return false; + return null; } private void abruptFlow(final PsiElement node) { @@ -1041,5 +1083,14 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { return !PsiTreeUtil.instanceOf(instruction.getElement(), PyStatementList.class); } + + private interface CallTypeKind { } + + private static class NoReturnCallKind implements CallTypeKind { + private NoReturnCallKind() {}; + public static final NoReturnCallKind INSTANCE = new NoReturnCallKind(); + } + + private record TypeGuardCallKind(@NotNull PyFunction pyFunction) implements CallTypeKind {} } diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java index a52c822c79b2..fb2250aa9beb 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java @@ -6,8 +6,10 @@ import com.intellij.psi.PsiElement; import com.intellij.util.containers.Stack; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeAnnotation; import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyEvaluator; import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.psi.types.*; @@ -46,6 +48,29 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor { } } + public void handleTypeGuardCall(@NotNull PyCallExpression call, @NotNull PyFunction function) { + if (call.getArguments().length == 0) return; + final var firstArgument = call.getArguments()[0]; + final var annotation = function.getAnnotationValue(); + if (annotation == null) return; + if (firstArgument instanceof PyReferenceExpression referenceExpression) { + pushAssertion(referenceExpression, myPositive, false, (context) -> { + var returnType = PyTypingTypeProvider.getReturnTypeAnnotation(function, context); + if (returnType instanceof PyStringLiteralExpression stringLiteralExpression) { + returnType = PyUtil.createExpressionFromFragment(stringLiteralExpression.getStringValue(), + function.getContainingFile()); + } + if (returnType instanceof PySubscriptionExpression subscriptionExpression) { + var indexExpression = subscriptionExpression.getIndexExpression(); + if (indexExpression != null) { + return Ref.deref(PyTypingTypeProvider.getType(indexExpression, context)); + } + } + return null; + }, null); + } + } + @Override public void visitPyCallExpression(@NotNull PyCallExpression node) { if (node.isCalleeText(PyNames.ISINSTANCE, PyNames.ASSERT_IS_INSTANCE)) { @@ -195,6 +220,9 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor { return type; } + /** + * @param transformToDefinition if true the result type will be Type[T], not T itself. + */ private void pushAssertion(@NotNull PyReferenceExpression target, boolean positive, boolean transformToDefinition, 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 a261351cf92a..1094ef1f1d58 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 @@ -62,6 +62,8 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext boolean typeHintedWithName(@NotNull T owner, @NotNull TypeEvalContext context, String... names) { - final PyExpression annotation = getAnnotationValue(owner, context); + var annotation = getAnnotationValue(owner, context); + if (annotation instanceof PyStringLiteralExpression stringLiteralExpression) { + final var annotationText = stringLiteralExpression.getStringValue(); + annotation = toExpression(annotationText, owner); + if (annotation == null) return false; + } + if (annotation instanceof PySubscriptionExpression) { return resolvesToQualifiedNames(((PySubscriptionExpression)annotation).getOperand(), context, names); } @@ -1165,6 +1173,11 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext + typeHintedWithName(function, context, TYPE_GUARD, TYPE_GUARD_EXT)); + } + private static boolean resolvesToQualifiedNames(@NotNull PyExpression expression, @NotNull TypeEvalContext context, String... names) { final var qualifiedNames = resolveToQualifiedNames(expression, context); return ContainerUtil.exists(names, qualifiedNames::contains); diff --git a/python/testData/codeInsight/controlflow/TypeGuard.py b/python/testData/codeInsight/controlflow/TypeGuard.py new file mode 100644 index 000000000000..13268711053b --- /dev/null +++ b/python/testData/codeInsight/controlflow/TypeGuard.py @@ -0,0 +1,13 @@ +from typing import List +from typing_extensions import TypeGuard +import foo + +def checkit(foo: List[int]) -> TypeGuard[List[str]]: + pass + +x = foo.bar() + +if checkit(x): + print(x) +else: + pass diff --git a/python/testData/codeInsight/controlflow/TypeGuard.txt b/python/testData/codeInsight/controlflow/TypeGuard.txt new file mode 100644 index 000000000000..52f763b222db --- /dev/null +++ b/python/testData/codeInsight/controlflow/TypeGuard.txt @@ -0,0 +1,29 @@ +0(1) element: null +1(2) element: PyFromImportStatement +2(3) WRITE ACCESS: List +3(4) element: PyFromImportStatement +4(5) WRITE ACCESS: TypeGuard +5(6) element: PyImportStatement +6(7) WRITE ACCESS: foo +7(8) element: PyFunction('checkit') +8(9) element: PySubscriptionExpression +9(10) READ ACCESS: List +10(11) READ ACCESS: int +11(12) element: PySubscriptionExpression +12(13) READ ACCESS: TypeGuard +13(14) element: PySubscriptionExpression +14(15) READ ACCESS: List +15(16) READ ACCESS: str +16(17) WRITE ACCESS: checkit +17(18) element: PyAssignmentStatement +18(19) READ ACCESS: foo +19(20) WRITE ACCESS: x +20(21) element: PyIfStatement +21(22) READ ACCESS: checkit +22(23,27) READ ACCESS: x +23(24) element: PyStatementList. Condition: checkit(x):true +24(25) ASSERTTYPE ACCESS: x +25(26) element: PyPrintStatement +26(28) READ ACCESS: x +27(28) element: PyStatementList. Condition: checkit(x):false +28() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeGuardConjunct.py b/python/testData/codeInsight/controlflow/TypeGuardConjunct.py new file mode 100644 index 000000000000..a01fb89fe24a --- /dev/null +++ b/python/testData/codeInsight/controlflow/TypeGuardConjunct.py @@ -0,0 +1,9 @@ +from typing import List +from typing_extensions import TypeGuard +import foo + +def checkit(foo: List[int]) -> TypeGuard[List[str]]: + pass + +x = foo.bar() +y = checkit(foo) and foo[123] \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeGuardConjunct.txt b/python/testData/codeInsight/controlflow/TypeGuardConjunct.txt new file mode 100644 index 000000000000..28b1e5634ae0 --- /dev/null +++ b/python/testData/codeInsight/controlflow/TypeGuardConjunct.txt @@ -0,0 +1,33 @@ +0(1) element: null +1(2) element: PyFromImportStatement +2(3) WRITE ACCESS: List +3(4) element: PyFromImportStatement +4(5) WRITE ACCESS: TypeGuard +5(6) element: PyImportStatement +6(7) WRITE ACCESS: foo +7(8) element: PyFunction('checkit') +8(9) element: PySubscriptionExpression +9(10) READ ACCESS: List +10(11) READ ACCESS: int +11(12) element: PySubscriptionExpression +12(13) READ ACCESS: TypeGuard +13(14) element: PySubscriptionExpression +14(15) READ ACCESS: List +15(16) READ ACCESS: str +16(17) WRITE ACCESS: checkit +17(18) element: PyAssignmentStatement +18(19) READ ACCESS: foo +19(20) WRITE ACCESS: x +20(21) element: PyAssignmentStatement +21(22) element: PyBinaryExpression +22(23) READ ACCESS: checkit +23(24,26) READ ACCESS: foo +24(31) element: null. Condition: checkit(foo):false +26(25) ASSERTTYPE ACCESS: foo +25(27) element: null. Condition: checkit(foo):true +27(28) element: PySubscriptionExpression +28(29,30) READ ACCESS: foo +29(31) element: null. Condition: foo[123]:false +30(31) element: null. Condition: foo[123]:true +31(32) WRITE ACCESS: y +32() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeGuardWhile.py b/python/testData/codeInsight/controlflow/TypeGuardWhile.py new file mode 100644 index 000000000000..fb20aec1b69a --- /dev/null +++ b/python/testData/codeInsight/controlflow/TypeGuardWhile.py @@ -0,0 +1,10 @@ +from typing import List +from typing_extensions import TypeGuard +import foo + +def checkit(foo: List[int]) -> TypeGuard[List[str]]: + pass + +x = foo.bar() +while checkit(x): + x = foo.bar() \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeGuardWhile.txt b/python/testData/codeInsight/controlflow/TypeGuardWhile.txt new file mode 100644 index 000000000000..3261318f0099 --- /dev/null +++ b/python/testData/codeInsight/controlflow/TypeGuardWhile.txt @@ -0,0 +1,29 @@ +0(1) element: null +1(2) element: PyFromImportStatement +2(3) WRITE ACCESS: List +3(4) element: PyFromImportStatement +4(5) WRITE ACCESS: TypeGuard +5(6) element: PyImportStatement +6(7) WRITE ACCESS: foo +7(8) element: PyFunction('checkit') +8(9) element: PySubscriptionExpression +9(10) READ ACCESS: List +10(11) READ ACCESS: int +11(12) element: PySubscriptionExpression +12(13) READ ACCESS: TypeGuard +13(14) element: PySubscriptionExpression +14(15) READ ACCESS: List +15(16) READ ACCESS: str +16(17) WRITE ACCESS: checkit +17(18) element: PyAssignmentStatement +18(19) READ ACCESS: foo +19(20) WRITE ACCESS: x +20(21) element: PyWhileStatement +21(22) READ ACCESS: checkit +22(23,28) READ ACCESS: x +23(24) element: PyStatementList. Condition: checkit(x):true +24(25) ASSERTTYPE ACCESS: x +25(26) element: PyAssignmentStatement +26(27) READ ACCESS: foo +27(20) WRITE ACCESS: x +28() element: null \ No newline at end of file diff --git a/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNoReturnWithQuotesFunction.py b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNoReturnWithQuotesFunction.py new file mode 100644 index 000000000000..f4e289df5cbd --- /dev/null +++ b/python/testData/inspections/PyUnreachableCodeInspection/UnreachableCodeReportedAfterNoReturnWithQuotesFunction.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/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index db097a8dff93..82da3193e8ce 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -1966,6 +1966,166 @@ public class Py3TypeTest extends PyTestCase { ); } + public void testTypeGuardList() { + doTest("list[str]", + """ + from typing import List + from typing import TypeGuard + + + def is_str_list(val: List[object]) -> TypeGuard[List[str]]: + return all(isinstance(x, str) for x in val) + + + def func1(val: List[object]): + if is_str_list(val): + expr = val + """); + } + + public void testTypeGuardListInStringLiteral() { + doTest("list[str]", + """ + from typing import List + from typing import TypeGuard + + + def is_str_list(val: List[object]) -> "TypeGuard[List[str]]": + return all(isinstance(x, str) for x in val) + + + def func1(val: List[object]): + if is_str_list(val): + expr = val + """); + } + + + public void testTypeGuardListTypeIsNotChanged() { + doTest("list[object]", + """ + from typing import List + from typing import TypeGuard + + + def is_str_list(val: List[object]) -> TypeGuard[List[str]]: + return all(isinstance(x, str) for x in val) + + + def func1(val: List[object]): + if is_str_list(val): + pass + else: + expr = val + """); + } + + public void testTypeGuardListNegation() { + doTest("list[str]", + """ + from typing import List + from typing import TypeGuard + + + def is_str_list(val: List[object]) -> TypeGuard[List[str]]: + return all(isinstance(x, str) for x in val) + + + def func1(val: List[object]): + if not is_str_list(val): + pass + else: + expr = val + """); + } + + // PY-62078 + public void ignoreTestTypeGuardAnnotation() { + doTest("list[str]", + """ + from typing import List + from typing import TypeGuard + + + def is_str_list(val): + # type: (List[object]) -> TypeGuard[List[str]] + return all(isinstance(x, str) for x in val) + + + def func1(val: List[object]): + if not is_str_list(val): + pass + else: + expr = val + """); + } + + public void testTypeGuardDidntChanged() { + doTest("list[object]", + """ + from typing import List + from typing import TypeGuard + + + def is_str_list(val: List[object]) -> TypeGuard[List[str]]: + return all(isinstance(x, str) for x in val) + + + def func1(val: List[object]): + if not is_str_list(val): + expr = val + else: + pass + """); + } + + public void testTypeGuardDoubleCheck() { + doTest("Person", + """ + from typing import TypeGuard + class Person(TypedDict): + name: str + age: int + + + def is_person(val: dict) -> TypeGuard[Person]: + try: + return isinstance(val["name"], str) and isinstance(val["age"], int) + except KeyError: + return False + + + def print_age(val: dict, val2: dict): + if is_person(val) and is_person(val2): + expr = val + else: + print("Not a person!")"""); + } + + public void testTypeGuardDoubleCheckNegation() { + doTest("Person", + """ + from typing import TypeGuard + class Person(TypedDict): + name: str + age: int + + + def is_person(val: dict) -> TypeGuard[Person]: + try: + return isinstance(val["name"], str) and isinstance(val["age"], int) + except KeyError: + return False + + + def print_age(val: dict, val2: dict): + if not is_person(val) or not is_person(val2): + print("Not a person!"); + else: + expr = val + """); + } + public void testDictCallOnDictLiteralResult() { doTest("dict[LiteralString, int]", "expr = dict({'a': 1})"); diff --git a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java index 02f577728e88..94f703ecb8c1 100644 --- a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java +++ b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java @@ -488,6 +488,18 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase { doTest(); } + public void testTypeGuard() { + doTest(); + } + + public void testTypeGuardConjunct() { + doTest(); + } + + public void testTypeGuardWhile() { + doTest(); + } + // PY-23859 public void testControlFlowIsAbruptAfterSelfFail() { final String testName = getTestName(false); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java index 6d948d471d03..38e80da1587a 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java @@ -228,6 +228,10 @@ public class PyUnreachableCodeInspectionTest extends PyInspectionTestCase { doTest(); } + public void testUnreachableCodeReportedAfterNoReturnWithQuotesFunction() { + doTest(); + } + // PY-24273 public void testUnreachableCodeReportedAfterImportedNoReturnFunction() { doMultiFileTest();