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 aede0642e1d0..6df8b235c797 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 @@ -16,9 +16,11 @@ package com.jetbrains.python.codeInsight.controlflow; import com.google.common.collect.ImmutableSet; +import com.intellij.codeInsight.controlflow.ConditionalInstruction; import com.intellij.codeInsight.controlflow.ControlFlow; import com.intellij.codeInsight.controlflow.ControlFlowBuilder; import com.intellij.codeInsight.controlflow.Instruction; +import com.intellij.codeInsight.controlflow.impl.ConditionalInstructionImpl; import com.intellij.openapi.util.Pair; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiNamedElement; @@ -308,8 +310,14 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { } @NotNull - private List> getPrevInstructions(@Nullable PyElement condition) { - final List> result = ContainerUtil.newArrayList(Pair.create(condition, myBuilder.prevInstruction)); + private List> getBranchingPoints(@Nullable PyExpression condition) { + final List> result = new ArrayList<>(); + + if (!isConjunctionOrDisjunction(condition)) { + // make previous instruction to be considered as a negative branching point + result.add(Pair.create(condition, myBuilder.prevInstruction)); + } + myBuilder.processPending((pendingScope, instruction) -> { if (pendingScope != null && PsiTreeUtil.isAncestor(condition, pendingScope, false)) { result.add(Pair.create(pendingScope, instruction)); @@ -350,7 +358,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { myBuilder.startNode(node); PyExpression lastCondition = null; // last visited condition - List> lastBranchingPoints = Collections.emptyList(); // outcoming edges from the last visited condition + List> lastBranchingPoints = Collections.emptyList(); // outcoming negative edges from the last condition final List conditionResults = new ArrayList<>(); // visited conditions results final PyIfPart firstIfPart = node.getIfPart(); @@ -426,18 +434,30 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { condition.accept(assertionEvaluator); } - final List> branchingPoints = getPrevInstructions(condition); - if (conditionResult != Boolean.FALSE) { - // edges to the statement list under `if` would be created below if condition were not evaluated to `False` - branchingPoints.forEach(pair -> myBuilder.addPendingEdge(pair.getFirst(), pair.getSecond())); + final List> branchingPoints = getBranchingPoints(condition); + if (conditionResult == Boolean.FALSE) { + myBuilder.flowAbrupted(); + } + else if (isConjunctionOrDisjunction(condition)) { + // to make edges between positive sub-conditions inside and the statement below + final var shortCircuitPositivePoints = selectShortCircuitPositivePoints(branchingPoints); + + branchingPoints.removeAll(shortCircuitPositivePoints); + shortCircuitPositivePoints.forEach(pair -> myBuilder.addPendingEdge(pair.getFirst(), pair.getSecond())); } - myBuilder.prevInstruction = null; visitPyIfPartStatements(part, assertionEvaluator, node); return new Triple<>(condition, branchingPoints, conditionResult); } + private static @NotNull List> selectShortCircuitPositivePoints(@NotNull List> branchingPoints) { + return ContainerUtil.filter( + branchingPoints, + it -> it.getSecond() instanceof ConditionalInstruction && ((ConditionalInstruction)it.getSecond()).getResult() + ); + } + private void visitPyIfPartStatements(@NotNull PyIfPart part, @NotNull PyTypeAssertionEvaluator assertionEvaluator, @NotNull PyIfStatement node) { @@ -463,23 +483,20 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { @Override public void visitPyBinaryExpression(@NotNull PyBinaryExpression node) { - final PyElementType operator = node.getOperator(); - if (operator == PyTokenTypes.AND_KEYWORD || operator == PyTokenTypes.OR_KEYWORD) { + if (isConjunctionOrDisjunction(node)) { + final boolean conjunction = node.getOperator() == PyTokenTypes.AND_KEYWORD; + myBuilder.startNode(node); - final PyTypeAssertionEvaluator assertionEvaluator = new PyTypeAssertionEvaluator(operator == PyTokenTypes.AND_KEYWORD); + final PyTypeAssertionEvaluator assertionEvaluator = new PyTypeAssertionEvaluator(conjunction); final PyExpression left = node.getLeftExpression(); if (left != null) { - left.accept(this); - left.accept(assertionEvaluator); - myBuilder.addPendingEdge(node, myBuilder.prevInstruction); + visitConjunctOrDisjunct(node, left, null, assertionEvaluator, conjunction); } final PyExpression right = node.getRightExpression(); if (right != null) { - InstructionBuilder.addAssertInstructions(myBuilder, assertionEvaluator); - right.accept(this); - myBuilder.addPendingEdge(node, myBuilder.prevInstruction); + visitConjunctOrDisjunct(node, right, assertionEvaluator, null, true); } } else { @@ -487,6 +504,41 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { } } + private static boolean isConjunctionOrDisjunction(@Nullable PyExpression node) { + if (node instanceof PyBinaryExpression) { + final var operator = ((PyBinaryExpression)node).getOperator(); + return operator == PyTokenTypes.AND_KEYWORD || operator == PyTokenTypes.OR_KEYWORD; + } + + return false; + } + + private void visitConjunctOrDisjunct(@NotNull PyBinaryExpression node, + @NotNull PyExpression subExpression, + @Nullable PyTypeAssertionEvaluator previousAssertionEvaluator, + @Nullable PyTypeAssertionEvaluator assertionEvaluator, + boolean conditionResultToContinue) { + if (previousAssertionEvaluator != null) { + InstructionBuilder.addAssertInstructions(myBuilder, previousAssertionEvaluator); + } + + subExpression.accept(this); + + if (assertionEvaluator != null) { + subExpression.accept(assertionEvaluator); + } + + final var branchingPoint = myBuilder.prevInstruction; + + final var outside = new ConditionalInstructionImpl(myBuilder, null, subExpression, !conditionResultToContinue); + myBuilder.addNode(outside); + myBuilder.addPendingEdge(node, outside); + + myBuilder.prevInstruction = branchingPoint; + final var toTheNext = new ConditionalInstructionImpl(myBuilder, null, subExpression, conditionResultToContinue); + myBuilder.addNode(toTheNext); + } + @Override public void visitPyWhileStatement(final @NotNull PyWhileStatement node) { final Instruction instruction = myBuilder.startNode(node); diff --git a/python/testData/codeInsight/controlflow/andbooleanexpression.txt b/python/testData/codeInsight/controlflow/andbooleanexpression.txt index 83abb003fb9a..ea39136f0e94 100644 --- a/python/testData/codeInsight/controlflow/andbooleanexpression.txt +++ b/python/testData/codeInsight/controlflow/andbooleanexpression.txt @@ -2,6 +2,10 @@ 1(2) element: PyAssignmentStatement 2(3) element: PyBinaryExpression 3(4,5) READ ACCESS: bar -4(5) READ ACCESS: baz -5(6) WRITE ACCESS: foo -6() element: null \ No newline at end of file +4(9) element: null. Condition: bar:false +5(6) element: null. Condition: bar:true +6(7,8) READ ACCESS: baz +7(9) element: null. Condition: baz:false +8(9) element: null. Condition: baz:true +9(10) WRITE ACCESS: foo +10() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/forreturn.txt b/python/testData/codeInsight/controlflow/forreturn.txt index f3faa655e705..5e493c9ce4f6 100644 --- a/python/testData/codeInsight/controlflow/forreturn.txt +++ b/python/testData/codeInsight/controlflow/forreturn.txt @@ -1,6 +1,6 @@ 0(1) element: null 1(2) element: PyForStatement -2(3,16) READ ACCESS: self +2(3,20) READ ACCESS: self 3(4) element: PyTupleExpression 4(5) WRITE ACCESS: start 5(6) WRITE ACCESS: end @@ -8,10 +8,14 @@ 7(8) element: PyIfStatement 8(9) element: PyBinaryExpression 9(10) READ ACCESS: hour -10(11,13,3,16) READ ACCESS: start -11(12) READ ACCESS: hour -12(13,3,16) READ ACCESS: end -13(14) element: PyStatementList. Condition: hour >= start and hour < end:true -14(15) element: PyReturnStatement -15(16) READ ACCESS: name -16() element: null \ No newline at end of file +10(11,12) READ ACCESS: start +11(3,20) element: null. Condition: hour >= start:false +12(13) element: null. Condition: hour >= start:true +13(14) READ ACCESS: hour +14(15,16) READ ACCESS: end +15(3,20) element: null. Condition: hour < end:false +16(17) element: null. Condition: hour < end:true +17(18) element: PyStatementList. Condition: hour >= start and hour < end:true +18(19) element: PyReturnStatement +19(20) READ ACCESS: name +20() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/function.txt b/python/testData/codeInsight/controlflow/function.txt index 689e69d58396..f4880fa7ead3 100644 --- a/python/testData/codeInsight/controlflow/function.txt +++ b/python/testData/codeInsight/controlflow/function.txt @@ -23,21 +23,29 @@ 22(23) element: PyBinaryExpression 23(24) element: PyBinaryExpression 24(25,26) READ ACCESS: collapse -25(26,27) element: PyLambdaExpression -26(27) element: PyLambdaExpression -27(28) WRITE ACCESS: processFunc -28(29) element: PyPrintStatement -29(30) element: PyListCompExpression -30(31) element: PyReferenceExpression: methodList -31(32,42) READ ACCESS: methodList -32(33) element: PyTargetExpression: method -33(34) WRITE ACCESS: method -34(35) element: PyBinaryExpression -35(36) READ ACCESS: method -36(37) READ ACCESS: spacing -37(38) READ ACCESS: processFunc -38(39) READ ACCESS: str -39(40) READ ACCESS: getattr -40(41) READ ACCESS: object -41(32,42) READ ACCESS: method -42() element: null \ No newline at end of file +25(32) element: null. Condition: collapse:false +26(27) element: null. Condition: collapse:true +27(28,29) element: PyLambdaExpression +28(32) element: null. Condition: (lambda s: " ".join(s.split())):false +29(30,31) element: null. Condition: (lambda s: " ".join(s.split())):true +30(35) element: null. Condition: collapse and (lambda s: " ".join(s.split())):true +31(32) element: null. Condition: collapse and (lambda s: " ".join(s.split())):false +32(33,34) element: PyLambdaExpression +33(35) element: null. Condition: (lambda s: s):false +34(35) element: null. Condition: (lambda s: s):true +35(36) WRITE ACCESS: processFunc +36(37) element: PyPrintStatement +37(38) element: PyListCompExpression +38(39) element: PyReferenceExpression: methodList +39(40,50) READ ACCESS: methodList +40(41) element: PyTargetExpression: method +41(42) WRITE ACCESS: method +42(43) element: PyBinaryExpression +43(44) READ ACCESS: method +44(45) READ ACCESS: spacing +45(46) READ ACCESS: processFunc +46(47) READ ACCESS: str +47(48) READ ACCESS: getattr +48(49) READ ACCESS: object +49(40,50) READ ACCESS: method +50() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/orbooleanexpression.txt b/python/testData/codeInsight/controlflow/orbooleanexpression.txt index 83abb003fb9a..54390a3729a0 100644 --- a/python/testData/codeInsight/controlflow/orbooleanexpression.txt +++ b/python/testData/codeInsight/controlflow/orbooleanexpression.txt @@ -2,6 +2,10 @@ 1(2) element: PyAssignmentStatement 2(3) element: PyBinaryExpression 3(4,5) READ ACCESS: bar -4(5) READ ACCESS: baz -5(6) WRITE ACCESS: foo -6() element: null \ No newline at end of file +4(9) element: null. Condition: bar:true +5(6) element: null. Condition: bar:false +6(7,8) READ ACCESS: baz +7(9) element: null. Condition: baz:false +8(9) element: null. Condition: baz:true +9(10) WRITE ACCESS: foo +10() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/typesinandbooleanexpression.txt b/python/testData/codeInsight/controlflow/typesinandbooleanexpression.txt index 2db18019d3a5..2ec6460fb480 100644 --- a/python/testData/codeInsight/controlflow/typesinandbooleanexpression.txt +++ b/python/testData/codeInsight/controlflow/typesinandbooleanexpression.txt @@ -8,10 +8,14 @@ 7(8) element: PyBinaryExpression 8(9) READ ACCESS: isinstance 9(10) READ ACCESS: var -10(11,13,15) READ ACCESS: A -11(12) ASSERTTYPE ACCESS: var -12(13,15) READ ACCESS: var -13(14) element: PyStatementList. Condition: isinstance(var, A) and var:true -14(16) ASSERTTYPE ACCESS: var -15(16) ASSERTTYPE ACCESS: var -16() element: null \ No newline at end of file +10(11,12) READ ACCESS: A +11(19) element: null. Condition: isinstance(var, A):false +12(13) element: null. Condition: isinstance(var, A):true +13(14) ASSERTTYPE ACCESS: var +14(15,16) READ ACCESS: var +15(19) element: null. Condition: var:false +16(17) element: null. Condition: var:true +17(18) element: PyStatementList. Condition: isinstance(var, A) and var:true +18(20) ASSERTTYPE ACCESS: var +19(20) ASSERTTYPE ACCESS: var +20() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/typesinorbooleanexpression.txt b/python/testData/codeInsight/controlflow/typesinorbooleanexpression.txt index 1a37a1046b22..388209958897 100644 --- a/python/testData/codeInsight/controlflow/typesinorbooleanexpression.txt +++ b/python/testData/codeInsight/controlflow/typesinorbooleanexpression.txt @@ -6,18 +6,22 @@ 5(6) element: PyIfStatement 6(7) READ ACCESS: isinstance 7(8) READ ACCESS: var -8(9,21) READ ACCESS: A +8(9,25) READ ACCESS: A 9(10) element: PyStatementList. Condition: isinstance(var, A):true 10(11) ASSERTTYPE ACCESS: var 11(12) element: PyIfStatement 12(13) element: PyBinaryExpression 13(14) READ ACCESS: isinstance 14(15) READ ACCESS: var -15(16,18,20) READ ACCESS: B -16(17) ASSERTTYPE ACCESS: var -17(18,20) READ ACCESS: var -18(19) element: PyStatementList. Condition: isinstance(var, B) or var:true -19(22) ASSERTTYPE ACCESS: var -20(22) ASSERTTYPE ACCESS: var -21(22) ASSERTTYPE ACCESS: var -22() element: null \ No newline at end of file +15(16,17) READ ACCESS: B +16(22) element: null. Condition: isinstance(var, B):true +17(18) element: null. Condition: isinstance(var, B):false +18(19) ASSERTTYPE ACCESS: var +19(20,21) READ ACCESS: var +20(24) element: null. Condition: var:false +21(22) element: null. Condition: var:true +22(23) element: PyStatementList. Condition: isinstance(var, B) or var:true +23(26) ASSERTTYPE ACCESS: var +24(26) ASSERTTYPE ACCESS: var +25(26) ASSERTTYPE ACCESS: var +26() element: null \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java index 95ba16291448..191932b7281c 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnboundLocalVariableInspectionTest.java @@ -347,6 +347,12 @@ public class PyUnboundLocalVariableInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-39262 + public void testAssignmentExpressionInsideAndInIf() { + doTestByText("if undefined1 and (r := undefined2()):\n" + + " print(r)"); + } + @NotNull @Override protected Class getInspectionClass() {