From fa138bf032b7d81960ac6e4baba86fedc0bada12 Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 25 Nov 2025 15:54:27 +0100 Subject: [PATCH] PY-50642 Take into account statically evaluated if-conditions when building CFG Unreachable if-branch no longer rejoins the main control flow to prevent visiting it when traversing CFG backwards (e.g., in `PyDefUseUtil.getLatestDefs()`). Also `typing.TYPE_CHECKING` checks are now taken into account when building CFG. GitOrigin-RevId: b67c71872c53aee556351e6e025a1091cdaae226 --- .../controlflow/PyControlFlowBuilder.java | 18 +++++++++++------- .../intentions/PyInvertIfConditionIntention.kt | 9 ++++++--- .../jetbrains/python/psi/impl/PyEvaluator.java | 14 ++++++++++++++ .../codeInsight/controlflow/IfElifFalse.txt | 4 ++-- .../codeInsight/controlflow/IfElifTrue.txt | 4 ++-- .../codeInsight/controlflow/IfFalse.txt | 6 +++--- .../codeInsight/controlflow/IfTrue.txt | 8 ++++---- .../controlflow/WhileInsideIfTrue.txt | 2 +- .../com/jetbrains/python/Py3TypeTest.java | 12 ++++++++++++ .../com/jetbrains/python/PyEvaluatorTest.java | 5 +++++ 10 files changed, 60 insertions(+), 22 deletions(-) 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 e73fcb96910d..c08d46f904d1 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 @@ -484,7 +484,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { myBuilder.startNode(node); List exitInstructions = new ArrayList<>(); - boolean unreachable = false; + boolean seenAlwaysTrueCondition = false; for (PyIfPart ifPart : StreamEx.of(node.getIfPart()).append(node.getElifParts())) { TransparentInstruction thenNode = addTransparentInstruction(); TransparentInstruction elseNode = addTransparentInstruction(); @@ -495,28 +495,32 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { myBuilder.prevInstruction = thenNode; Boolean conditionResult = PyEvaluator.evaluateAsBooleanNoResolve(condition); - if (unreachable || Boolean.FALSE.equals(conditionResult)) { - // Condition is always False, or some previous condition is always True. + boolean unreachable = seenAlwaysTrueCondition || Boolean.FALSE.equals(conditionResult); + if (unreachable) { addAssertTypeNever(); } if (Boolean.TRUE.equals(conditionResult)) { - unreachable = true; + seenAlwaysTrueCondition = true; } visitPyStatementPart(ifPart); - exitInstructions.add(myBuilder.prevInstruction); + if (!unreachable) { + exitInstructions.add(myBuilder.prevInstruction); + } myBuilder.prevInstruction = elseNode; } final PyElsePart elsePart = node.getElsePart(); if (elsePart != null) { - if (unreachable) { + if (seenAlwaysTrueCondition) { addAssertTypeNever(); } visitPyStatementPart(elsePart); } - exitInstructions.add(myBuilder.prevInstruction); + if (!seenAlwaysTrueCondition) { + exitInstructions.add(myBuilder.prevInstruction); + } myBuilder.prevInstruction = addTransparentInstruction(node); for (Instruction exitInstruction : Lists.reverse(exitInstructions)) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyInvertIfConditionIntention.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyInvertIfConditionIntention.kt index cd3c50acb873..cefd788b8603 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyInvertIfConditionIntention.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyInvertIfConditionIntention.kt @@ -274,15 +274,18 @@ class PyInvertIfConditionIntention : PsiUpdateModCommandAction(PsiEl private val PyStatementList.isTerminated: Boolean get() { val controlFlow = ControlFlowCache.getControlFlow(parentsOfType().first()) - val currentElement = this - val currentInstruction = controlFlow.instructions.first { it.element == currentElement } + val currentInstruction = controlFlow.instructions.first { it.element == this } var result = true ControlFlowUtil.iterate(currentInstruction.num(), controlFlow.instructions, { instruction -> when { + instruction.allSucc().isEmpty() -> { + result = false + ControlFlowUtil.Operation.BREAK + } instruction == currentInstruction -> ControlFlowUtil.Operation.NEXT instruction is ReadWriteInstruction -> ControlFlowUtil.Operation.NEXT instruction.element == null -> ControlFlowUtil.Operation.NEXT - !instruction.element!!.parents(false).contains(currentElement) -> { + !instruction.element!!.parents(false).contains(this) -> { result = false ControlFlowUtil.Operation.BREAK } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyEvaluator.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyEvaluator.java index 5fdbc91751e1..9b10e0a5eb64 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyEvaluator.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyEvaluator.java @@ -18,6 +18,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.psi.PsiElement; import com.intellij.psi.ResolveResult; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.QualifiedName; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; @@ -40,6 +41,11 @@ import java.util.*; */ public class PyEvaluator { + private static final List TYPING_TYPE_CHECKING_NAMES = List.of( + QualifiedName.fromDottedString("TYPE_CHECKING"), + QualifiedName.fromDottedString("typing.TYPE_CHECKING") + ); + private final @NotNull Set myVisited = new HashSet<>(); private @Nullable Map myNamespace = null; @@ -253,6 +259,9 @@ public class PyEvaluator { } protected @Nullable Object evaluateReference(@NotNull PyReferenceExpression expression) { + if (isTypeCheckingExpression(expression)) { + return true; + } if (!expression.isQualified()) { if (myNamespace != null) { return myNamespace.get(expression.getReferencedName()); @@ -456,4 +465,9 @@ public class PyEvaluator { } return myAllowExpressionsAsValues ? expression : null; } + + private static boolean isTypeCheckingExpression(@NotNull PyReferenceExpression expression) { + QualifiedName qualifiedName = expression.asQualifiedName(); + return qualifiedName != null && TYPING_TYPE_CHECKING_NAMES.contains(qualifiedName); + } } diff --git a/python/testData/codeInsight/controlflow/IfElifFalse.txt b/python/testData/codeInsight/controlflow/IfElifFalse.txt index 0c8887643041..2582b4c7931c 100644 --- a/python/testData/codeInsight/controlflow/IfElifFalse.txt +++ b/python/testData/codeInsight/controlflow/IfElifFalse.txt @@ -13,7 +13,7 @@ 12(13) ASSERTTYPE ACCESS: null 13(14) element: PyStatementList 14(15) element: PyAssignmentStatement -15(16) WRITE ACCESS: a +15() WRITE ACCESS: a 16(17) element: PyIfStatement 17(18,19) READ ACCESS: d 18(23) element: null. Condition: d:false @@ -28,7 +28,7 @@ 27(28) ASSERTTYPE ACCESS: null 28(29) element: PyStatementList 29(30) element: PyAssignmentStatement -30(33) WRITE ACCESS: b +30() WRITE ACCESS: b 31(32) element: PyStatementList 32(33) element: PyPassStatement 33() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/IfElifTrue.txt b/python/testData/codeInsight/controlflow/IfElifTrue.txt index 583c4fc3df23..1e33dbd7ad9b 100644 --- a/python/testData/codeInsight/controlflow/IfElifTrue.txt +++ b/python/testData/codeInsight/controlflow/IfElifTrue.txt @@ -7,7 +7,7 @@ 6(7) element: PyStatementList 7(14) element: PyPassStatement 8(9,10) READ ACCESS: True -9(14) element: null. Condition: True:false +9() element: null. Condition: True:false 10(11) element: null. Condition: True:true 11(12) ASSERTTYPE ACCESS: True 12(13) element: PyStatementList @@ -28,5 +28,5 @@ 27(28) ASSERTTYPE ACCESS: null 28(29) element: PyStatementList 29(30) element: PyAssignmentStatement -30(31) WRITE ACCESS: e +30() WRITE ACCESS: e 31() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/IfFalse.txt b/python/testData/codeInsight/controlflow/IfFalse.txt index a9edc6521b89..2ff725b45743 100644 --- a/python/testData/codeInsight/controlflow/IfFalse.txt +++ b/python/testData/codeInsight/controlflow/IfFalse.txt @@ -7,7 +7,7 @@ 6(7) ASSERTTYPE ACCESS: null 7(8) element: PyStatementList 8(9) element: PyAssignmentStatement -9(10) WRITE ACCESS: a +9() WRITE ACCESS: a 10(11) element: PyIfStatement 11(12,13) READ ACCESS: False 12(19) element: null. Condition: False:false @@ -16,7 +16,7 @@ 15(16) ASSERTTYPE ACCESS: null 16(17) element: PyStatementList 17(18) element: PyAssignmentStatement -18(21) WRITE ACCESS: b +18() WRITE ACCESS: b 19(20) element: PyStatementList 20(21) element: PyPassStatement 21(22) element: PyIfStatement @@ -27,7 +27,7 @@ 26(27) ASSERTTYPE ACCESS: null 27(28) element: PyStatementList 28(29) element: PyAssignmentStatement -29(38) WRITE ACCESS: c +29() WRITE ACCESS: c 30(31,32) READ ACCESS: d 31(36) element: null. Condition: d:false 32(33) element: null. Condition: d:true diff --git a/python/testData/codeInsight/controlflow/IfTrue.txt b/python/testData/codeInsight/controlflow/IfTrue.txt index 15f53b21f3b2..904625c6e613 100644 --- a/python/testData/codeInsight/controlflow/IfTrue.txt +++ b/python/testData/codeInsight/controlflow/IfTrue.txt @@ -1,7 +1,7 @@ 0(1) element: null 1(2) element: PyIfStatement 2(3,4) READ ACCESS: True -3(8) element: null. Condition: True:false +3() element: null. Condition: True:false 4(5) element: null. Condition: True:true 5(6) ASSERTTYPE ACCESS: True 6(7) element: PyStatementList @@ -16,7 +16,7 @@ 15(16) ASSERTTYPE ACCESS: null 16(17) element: PyStatementList 17(18) element: PyAssignmentStatement -18(19) WRITE ACCESS: b +18() WRITE ACCESS: b 19(20) element: PyIfStatement 20(21,22) READ ACCESS: True 21(26) element: null. Condition: True:false @@ -31,9 +31,9 @@ 30(31) ASSERTTYPE ACCESS: null 31(32) element: PyStatementList 32(33) element: PyAssignmentStatement -33(38) WRITE ACCESS: d +33() WRITE ACCESS: d 34(35) ASSERTTYPE ACCESS: null 35(36) element: PyStatementList 36(37) element: PyAssignmentStatement -37(38) WRITE ACCESS: e +37() WRITE ACCESS: e 38() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/WhileInsideIfTrue.txt b/python/testData/codeInsight/controlflow/WhileInsideIfTrue.txt index 62346a5c1458..7bb38cf19b46 100644 --- a/python/testData/codeInsight/controlflow/WhileInsideIfTrue.txt +++ b/python/testData/codeInsight/controlflow/WhileInsideIfTrue.txt @@ -14,5 +14,5 @@ 13(17) element: PyBreakStatement 14(15) ASSERTTYPE ACCESS: null 15(16) element: PyStatementList -16(17) element: PyPrintStatement +16() element: PyPrintStatement 17() element: null \ 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 7ccbb26f3c79..9ab1fab7a9cc 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -3335,6 +3335,18 @@ public class Py3TypeTest extends PyTestCase { }); } + // PY-50642 + public void testTypeChecking() { + doTest("int", """ + from typing import TYPE_CHECKING + + if not not TYPE_CHECKING: + v: int = -1 + else: + v: str = 'ab' + expr = v + """); + } // PY-73958 public void testNoStackOverflow() { diff --git a/python/testSrc/com/jetbrains/python/PyEvaluatorTest.java b/python/testSrc/com/jetbrains/python/PyEvaluatorTest.java index 2c0fb09e6641..bf849d7960e5 100644 --- a/python/testSrc/com/jetbrains/python/PyEvaluatorTest.java +++ b/python/testSrc/com/jetbrains/python/PyEvaluatorTest.java @@ -314,6 +314,11 @@ public class PyEvaluatorTest extends PyTestCase { assertNull(PyEvaluator.evaluateAsBooleanNoResolve(parseText("a = []\nexpr = a"))); } + public void testTypingTypeChecking() { + assertTrue(PyEvaluator.evaluateAsBooleanNoResolve(parseText("expr = typing.TYPE_CHECKING"))); + assertTrue(PyEvaluator.evaluateAsBooleanNoResolve(parseText("expr = TYPE_CHECKING"))); + } + @NotNull private T byExpression(@NotNull String expression, @NotNull Class cls) { final Object value = new PyEvaluator().evaluate(parseExpression(expression));