From 66599474378c53fc04ab96399ccf945481f23b7c Mon Sep 17 00:00:00 2001 From: "Aleksandr.Govenko" Date: Fri, 14 Feb 2025 13:18:20 +0000 Subject: [PATCH] PY-51564 "code is unreachable" warning does not consider that context managers can abort Added PyWithContextExitInstruction that works as 'except' node in try-except statements, but for 'with context-manager' statements. This allows to see when it can recover from exception by checking the type of `contextlib.AbstractContextManager` Merge-request: IJ-MR-152213 Merged-by: Aleksandr Govenko GitOrigin-RevId: fd5a139de31d47136cc86b4a7e090a20b73b8744 --- .../controlflow/CallInstruction.kt | 6 - .../controlflow/PyControlFlowBuilder.java | 130 +++++++-------- .../controlflow/PyRaiseInstruction.kt | 9 +- .../PyWithContextExitInstruction.kt | 29 ++++ .../typing/PyTypingTypeProvider.java | 5 +- .../python/inspections/PyInspectionsUtil.java | 64 +++++-- .../PyUnboundLocalVariableInspection.java | 1 + .../PyUnreachableCodeInspection.java | 31 +--- .../PyUnusedLocalInspectionVisitor.java | 1 + .../python/psi/impl/PyFunctionImpl.java | 8 +- .../python/psi/types/PyTypeChecker.java | 10 ++ .../python/psi/types/PyTypeUtil.java | 15 ++ .../python/refactoring/PyDefUseUtil.java | 11 +- .../codeInsight/controlflow/Assert.txt | 2 +- .../ContinueInPositiveIteration.txt | 2 +- .../codeInsight/controlflow/DoubleTry.txt | 2 +- .../codeInsight/controlflow/IfElseReturn.txt | 2 +- .../codeInsight/controlflow/Raise.txt | 4 +- .../testData/codeInsight/controlflow/Try.txt | 2 +- .../codeInsight/controlflow/TryBreak.txt | 4 +- .../controlflow/TryRaiseFinally.txt | 2 +- .../codeInsight/controlflow/TryTry.txt | 6 +- .../codeInsight/controlflow/WithAssert.txt | 25 +-- .../controlflow/WithAssertFalse.txt | 17 +- .../controlflow/WithRaiseException.txt | 31 ++-- .../controlflow/WithSeveralContextsAssert.py | 6 + .../controlflow/WithSeveralContextsAssert.txt | 15 ++ .../UnionReturnTypes.py | 2 +- .../Unreachable.py | 2 + .../WithSuppressedExceptions.py | 58 ------- .../python/PyControlFlowBuilderTest.java | 5 + .../python/PyTypeConversionTest.java | 74 +++++++++ .../PyUnreachableCodeInspectionTest.java | 156 +++++++++++++++--- .../PyUnusedLocalInspectionTest.java | 2 +- 34 files changed, 477 insertions(+), 262 deletions(-) create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt create mode 100644 python/testData/codeInsight/controlflow/WithSeveralContextsAssert.py create mode 100644 python/testData/codeInsight/controlflow/WithSeveralContextsAssert.txt delete mode 100644 python/testData/inspections/PyUnreachableCodeInspection/WithSuppressedExceptions.py create mode 100644 python/testSrc/com/jetbrains/python/PyTypeConversionTest.java diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt index b7d8029e95b8..4452546b1da8 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt @@ -24,10 +24,4 @@ class CallInstruction(builder: ControlFlowBuilder, call: PyCallExpression) : Ins } return false } - - companion object { - fun allPredWithoutNoReturn(instruction: Instruction, typeEvalContext: TypeEvalContext): List { - return instruction.allPred().filter { it !is CallInstruction || !it.isNoReturnCall(typeEvalContext) } - } - } } \ No newline at end of file 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 31a6990f08ae..ca0011728841 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 @@ -15,7 +15,6 @@ */ package com.jetbrains.python.codeInsight.controlflow; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.intellij.codeInsight.controlflow.ControlFlow; import com.intellij.codeInsight.controlflow.ControlFlowBuilder; @@ -39,12 +38,9 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Set; public class PyControlFlowBuilder extends PyRecursiveElementVisitor { - private static final @NotNull Set EXCEPTION_SUPPRESSORS = ImmutableSet.of("suppress", "assertRaises", "assertRaisesRegex"); - private final ControlFlowBuilder myBuilder = new ControlFlowBuilder(); private @Nullable TrueFalseNodes myTrueFalseNodes; @@ -312,6 +308,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { myBuilder.startNode(node); TransparentInstruction trueNode = addTransparentInstruction(); TransparentInstruction falseNode = addTransparentInstruction(); + TransparentInstruction exitNode = addTransparentInstruction(); final PyExpression condition = node.getCondition(); if (condition != null) { visitCondition(condition, trueNode, falseNode); @@ -321,13 +318,14 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { if (truePart != null) { myBuilder.prevInstruction = trueNode; truePart.accept(this); - myBuilder.addPendingEdge(node, myBuilder.prevInstruction); + myBuilder.addEdge(myBuilder.prevInstruction, exitNode); } if (falsePart != null) { myBuilder.prevInstruction = falseNode; falsePart.accept(this); - myBuilder.addPendingEdge(node, myBuilder.prevInstruction); + myBuilder.addEdge(myBuilder.prevInstruction, exitNode); } + myBuilder.prevInstruction = exitNode; } @Override @@ -569,16 +567,6 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { for (PyExpression expression : expressions) { expression.accept(this); } - - myBuilder.processPending((pendingScope, instruction) -> { - final PsiElement pendingElement = instruction.getElement(); - if (pendingElement != null && PsiTreeUtil.isAncestor(node, pendingElement, false)) { - myBuilder.addEdge(null, instruction); - } - else { - myBuilder.addPendingEdge(pendingScope, instruction); - } - }); myBuilder.addPendingEdge(null, myBuilder.prevInstruction); myBuilder.flowAbrupted(); } @@ -590,7 +578,8 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { if (expression != null) { expression.accept(this); } - abruptFlow(node); + myBuilder.addPendingEdge(null, myBuilder.prevInstruction); + myBuilder.flowAbrupted(); } @Override @@ -822,25 +811,16 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { // assert False if (args.length >= 1) { if (!PyEvaluator.evaluateAsBooleanNoResolve(args[0], true)) { - myBuilder.processPending((pendingScope, instruction) -> { - final PsiElement pendingElement = instruction.getElement(); - if (pendingElement != null && PsiTreeUtil.isAncestor(node, pendingElement, false)) { - myBuilder.addEdge(null, instruction); - } - else { - myBuilder.addPendingEdge(pendingScope, instruction); - } - }); myBuilder.addPendingEdge(null, myBuilder.prevInstruction); - myBuilder.flowAbrupted(); - return; - } + myBuilder.flowAbrupted(); + return; + } TransparentInstruction trueNode = addTransparentInstruction(); TransparentInstruction falseNode = addTransparentInstruction(); visitCondition(args[0], trueNode, falseNode); - PyRaiseInstruction raiseInstruction = new PyRaiseInstruction(myBuilder, null); + PyRaiseInstruction raiseInstruction = new PyRaiseInstruction(myBuilder, node); myBuilder.instructions.add(raiseInstruction); myBuilder.addEdge(falseNode, raiseInstruction); @@ -857,28 +837,54 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { @Override public void visitPyWithStatement(final @NotNull PyWithStatement node) { - super.visitPyWithStatement(node); + myBuilder.startNode(node); - final boolean suppressor = StreamEx - .of(node.getWithItems()) - .map(PyWithItem::getExpression) - .select(PyCallExpression.class) - .map(PyCallExpression::getCallee) - .select(PyReferenceExpression.class) - .anyMatch(it -> EXCEPTION_SUPPRESSORS.contains(it.getReferencedName())); - - myBuilder.processPending((pendingScope, instruction) -> { - final PsiElement element = instruction.getElement(); - if (element != null && - PsiTreeUtil.isAncestor(node, element, true) && - (suppressor && canRaiseExceptions(instruction) - || PsiTreeUtil.getParentOfType(element, PyRaiseStatement.class) != null - || PsiTreeUtil.getParentOfType(element, PyAssertStatement.class) != null - )) { - myBuilder.addPendingEdge(node, instruction); + List exits = new ArrayList<>(); + for (var item : node.getWithItems()) { + int itemStart = myBuilder.instructions.size(); + item.accept(this); + int itemEnd = myBuilder.instructions.size(); + for (int i = itemStart; i < itemEnd; i++) { + final Instruction instruction = myBuilder.instructions.get(i); + final PsiElement e = instruction.getElement(); + if (e == null || !canRaiseExceptions(instruction) || !PsiTreeUtil.isAncestor(node, e, false)) { + continue; + } + for (var exit : exits) { + myBuilder.addEdge(myBuilder.instructions.get(i), exit); + } } - myBuilder.addPendingEdge(pendingScope, instruction); - }); + + var nextExit = new PyWithContextExitInstruction(myBuilder, item); + exits.add(nextExit); + myBuilder.instructions.add(nextExit); + // ControlFlowUtil.iterate assumes nodes are added to CFG in order they are created + } + + final var toAllExits = addTransparentInstruction(); + final var fromAllExits = addTransparentInstruction(); + for (var exit : exits) { + myBuilder.addEdge(toAllExits, exit); + myBuilder.addEdge(exit, fromAllExits); + } + + int stmtStart = myBuilder.instructions.size(); + node.getStatementList().accept(this); + int stmtEnd = myBuilder.instructions.size(); + + for (int j = stmtStart; j < stmtEnd; j++) { + final Instruction instruction = myBuilder.instructions.get(j); + final PsiElement e = instruction.getElement(); + if (e == null || !canRaiseExceptions(instruction) || !PsiTreeUtil.isAncestor(node, e, false)) { + continue; + } + myBuilder.addEdge(instruction, toAllExits); + } + + // Checks if exit nodes will have at least one predecessor + if (exits.size() > 1 || !toAllExits.allPred().isEmpty()) { + myBuilder.addPendingEdge(node, fromAllExits); + } } @Override @@ -935,26 +941,16 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { statementList.accept(this); } - private void abruptFlow(final PsiElement node) { - // Here we process pending instructions!!! - myBuilder.processPending((pendingScope, instruction) -> { - if (pendingScope != null && PsiTreeUtil.isAncestor(node, pendingScope, false)) { - myBuilder.addPendingEdge(null, instruction); - } - else { - myBuilder.addPendingEdge(pendingScope, instruction); - } - }); - myBuilder.addPendingEdge(null, myBuilder.prevInstruction); - myBuilder.flowAbrupted(); - } - - private static boolean canRaiseExceptions(final Instruction instruction) { + private static boolean canRaiseExceptions(@NotNull Instruction instruction) { if (instruction instanceof ReadWriteInstruction) { return true; } - return !PsiTreeUtil.instanceOf(instruction.getElement(), - PyStatementList.class); + PsiElement element = instruction.getElement(); + return !(element instanceof PyReturnStatement returnStatement && returnStatement.getExpression() == null + || element instanceof PyContinueStatement + || element instanceof PyBreakStatement + || element instanceof PyPassStatement + || element instanceof PyStatementList); } private void addTypeAssertionNodes(@NotNull PyExpression condition, boolean positive) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyRaiseInstruction.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyRaiseInstruction.kt index 4c0df90933f7..3073b54a5c04 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyRaiseInstruction.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyRaiseInstruction.kt @@ -2,6 +2,13 @@ package com.jetbrains.python.codeInsight.controlflow import com.intellij.codeInsight.controlflow.ControlFlowBuilder import com.intellij.codeInsight.controlflow.impl.InstructionImpl +import com.jetbrains.python.psi.PyAssertStatement import com.jetbrains.python.psi.PyRaiseStatement +import com.jetbrains.python.psi.PyStatement -class PyRaiseInstruction(builder: ControlFlowBuilder, raiseStatement: PyRaiseStatement?): InstructionImpl(builder, raiseStatement) \ No newline at end of file +class PyRaiseInstruction private constructor(builder: ControlFlowBuilder, raiseOrAssert: PyStatement): InstructionImpl(builder, raiseOrAssert) { + constructor(builder: ControlFlowBuilder, raise: PyRaiseStatement): this(builder, raise as PyStatement) + constructor(builder: ControlFlowBuilder, assert: PyAssertStatement): this(builder, assert as PyStatement) + + override fun getElementPresentation(): String = "raise: ${element}" +} \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt new file mode 100644 index 000000000000..4d0dc8eb7b88 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyWithContextExitInstruction.kt @@ -0,0 +1,29 @@ +package com.jetbrains.python.codeInsight.controlflow + +import com.intellij.codeInsight.controlflow.ControlFlowBuilder +import com.intellij.codeInsight.controlflow.impl.InstructionImpl +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.psi.PyWithItem +import com.jetbrains.python.psi.PyWithStatement +import com.jetbrains.python.psi.impl.PyBuiltinCache +import com.jetbrains.python.psi.types.PyCollectionType +import com.jetbrains.python.psi.types.PyTypeUtil +import com.jetbrains.python.psi.types.TypeEvalContext + +class PyWithContextExitInstruction(builder: ControlFlowBuilder, withItem: PyWithItem): InstructionImpl(builder, withItem) { + override fun getElementPresentation(): String = "exit context manager: ${element.text}" + override fun getElement(): PyWithItem = super.getElement() as PyWithItem + + /** + * While traversing CFG, use this method to know if you should let your traversal consider this node. + * Usually, you would want it only if the context manager DOES suppress exceptions. + */ + fun isSuppressingExceptions(context: TypeEvalContext): Boolean { + val withStmt = PsiTreeUtil.getParentOfType(element, PyWithStatement::class.java, false) ?: return false + val abstractType = if (withStmt.isAsync) "contextlib.AbstractAsyncContextManager" else "contextlib.AbstractContextManager" + return context.getType(element.expression) + .let { PyTypeUtil.convertToType(it, abstractType, element, context) } + .let { (it as? PyCollectionType)?.elementTypes?.getOrNull(1) } + .let { it == PyBuiltinCache.getInstance(element).boolType } + } +} \ No newline at end of file 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 418cd30b9116..39b11e34b9b4 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 @@ -128,7 +128,10 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< PARAM_SPEC, PARAM_SPEC_EXT, TYPE_VAR_TUPLE, TYPE_VAR_TUPLE_EXT ); - + + public static final String CONTEXT_MANAGER = "contextlib.AbstractContextManager"; + public static final String ASYNC_CONTEXT_MANAGER = "contextlib.AbstractAsyncContextManager"; + public static final Set TYPE_DICT_QUALIFIERS = Set.of(REQUIRED, REQUIRED_EXT, NOT_REQUIRED, NOT_REQUIRED_EXT, READONLY, READONLY_EXT); public static final String UNPACK = "typing.Unpack"; diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInspectionsUtil.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInspectionsUtil.java index d676cb28e726..9ca6dd0c1724 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInspectionsUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInspectionsUtil.java @@ -4,42 +4,72 @@ package com.jetbrains.python.inspections; import com.intellij.codeInsight.controlflow.ControlFlow; import com.intellij.codeInsight.controlflow.ControlFlowUtil; import com.intellij.codeInsight.controlflow.Instruction; -import com.intellij.openapi.util.Ref; import com.intellij.psi.PsiElement; +import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.codeInsight.controlflow.CallInstruction; import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; +import com.jetbrains.python.codeInsight.controlflow.PyWithContextExitInstruction; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; +import com.jetbrains.python.psi.PyStatementListContainer; import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.List; public final class PyInspectionsUtil { @ApiStatus.Internal public static boolean hasAnyInterruptedControlFlowPaths(@NotNull PsiElement element, @NotNull TypeEvalContext context) { final ScopeOwner owner = ScopeUtil.getScopeOwner(element); if (owner != null) { - final ControlFlow flow = ControlFlowCache.getControlFlow(owner); - final Instruction[] instructions = flow.getInstructions(); - final int start = ControlFlowUtil.findInstructionNumberByElement(instructions, element); - if (start >= 0) { - final Ref resultRef = Ref.create(false); - ControlFlowUtil.iteratePrev(start, instructions, instruction -> { - if (CallInstruction.Companion.allPredWithoutNoReturn(instruction, context).isEmpty() && !isFirstInstruction(instruction)) { - resultRef.set(true); - return ControlFlowUtil.Operation.BREAK; - } - return ControlFlowUtil.Operation.NEXT; - }); - return resultRef.get(); - } + return !collectUnreachable(owner, element, context).isEmpty(); } return false; } + /** + * Collects a list of unreachable elements, iterating through CFG backwards + * + * @param anchor the anchor element to start the iteration from, can be null to start from the last instruction + */ @ApiStatus.Internal - static boolean isFirstInstruction(Instruction instruction) { - return instruction.num() == 0; + public static @NotNull List collectUnreachable(@NotNull ScopeOwner owner, @Nullable PsiElement anchor, @NotNull TypeEvalContext context) { + final ControlFlow flow = ControlFlowCache.getControlFlow(owner); + final Instruction[] instructions = flow.getInstructions(); + final List unreachable = new ArrayList<>(); + final int start = anchor != null ? ControlFlowUtil.findInstructionNumberByElement(instructions, anchor) : instructions.length - 1; + if (start >= 0) { + ControlFlowUtil.iteratePrev(start, instructions, instruction -> { + if (getReachablePredecessors(instruction, context).isEmpty() && instruction.num() != 0) { + ContainerUtil.addIfNotNull(unreachable, getRelevantElement(instruction)); + } + return ControlFlowUtil.Operation.NEXT; + }); + } + return unreachable; + } + + private static @NotNull List getReachablePredecessors(@NotNull Instruction instruction, @NotNull TypeEvalContext context) { + return ContainerUtil.filter(instruction.allPred(), it -> { + if (it instanceof CallInstruction ci && ci.isNoReturnCall(context)) return false; + if (it instanceof PyWithContextExitInstruction wi && !wi.isSuppressingExceptions(context)) return false; + return true; + }); + } + + private static @Nullable PsiElement getRelevantElement(@NotNull Instruction instruction) { + if (instruction instanceof PyWithContextExitInstruction) { + return null; + } + PsiElement element = instruction.getElement(); + if (element instanceof PyStatementListContainer) { + return ((PyStatementListContainer)element).getStatementList(); + } + return element; } private PyInspectionsUtil() { diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java index f30a6a08fd31..c3805a82fae7 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java @@ -15,6 +15,7 @@ import com.intellij.psi.PsiPolyVariantReference; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.python.PyPsiBundle; import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; +import com.jetbrains.python.codeInsight.controlflow.PyWithContextExitInstruction; import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.Scope; diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnreachableCodeInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnreachableCodeInspection.java index a7e4a16c7992..0c9718ab4078 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnreachableCodeInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnreachableCodeInspection.java @@ -1,24 +1,14 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.inspections; -import com.intellij.codeInsight.controlflow.ControlFlow; -import com.intellij.codeInsight.controlflow.ControlFlowUtil; -import com.intellij.codeInsight.controlflow.Instruction; import com.intellij.codeInspection.LocalInspectionToolSession; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.jetbrains.python.PyPsiBundle; -import com.jetbrains.python.codeInsight.controlflow.CallInstruction; -import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; -import com.jetbrains.python.psi.PyStatementListContainer; import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; /** * Detects unreachable code using control flow graph @@ -38,27 +28,12 @@ public final class PyUnreachableCodeInspection extends PyInspection { } @Override - public void visitElement(final @NotNull PsiElement element) { - if (element instanceof ScopeOwner) { - final ControlFlow flow = ControlFlowCache.getControlFlow((ScopeOwner)element); - final Instruction[] instructions = flow.getInstructions(); - final List unreachable = new ArrayList<>(); - if (instructions.length > 0) { - ControlFlowUtil.iteratePrev(instructions.length - 1, instructions, instruction -> { - if (CallInstruction.Companion.allPredWithoutNoReturn(instruction, myTypeEvalContext).isEmpty() && !PyInspectionsUtil.isFirstInstruction(instruction)) { - unreachable.add(unwrapStatementListContainer(instruction.getElement())); - } - return ControlFlowUtil.Operation.NEXT; - }); - } - for (PsiElement e : unreachable) { + public void visitElement(@NotNull final PsiElement element) { + if (element instanceof ScopeOwner owner) { + for (PsiElement e : PyInspectionsUtil.collectUnreachable(owner, null, myTypeEvalContext)) { registerProblem(e, PyPsiBundle.message("INSP.unreachable.code")); } } } - - private static @Nullable PsiElement unwrapStatementListContainer(@Nullable PsiElement element) { - return element instanceof PyStatementListContainer ? ((PyStatementListContainer)element).getStatementList() : element; - } } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/unusedLocal/PyUnusedLocalInspectionVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/unusedLocal/PyUnusedLocalInspectionVisitor.java index 85ed887def4a..0ed24ed9a5c0 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/unusedLocal/PyUnusedLocalInspectionVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/unusedLocal/PyUnusedLocalInspectionVisitor.java @@ -22,6 +22,7 @@ import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyPsiBundle; import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; +import com.jetbrains.python.codeInsight.controlflow.PyWithContextExitInstruction; import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.Scope; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index 337caa635d67..3830a5324af7 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -22,10 +22,7 @@ import com.intellij.util.PlatformIcons; import com.intellij.util.containers.JBIterable; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyStubElementTypes; -import com.jetbrains.python.codeInsight.controlflow.CallInstruction; -import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; -import com.jetbrains.python.codeInsight.controlflow.PyRaiseInstruction; -import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.codeInsight.controlflow.*; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.documentation.docstrings.DocStringUtil; @@ -391,6 +388,9 @@ public class PyFunctionImpl extends PyBaseElementImpl implements if (instruction instanceof PyRaiseInstruction) { return ControlFlowUtil.Operation.CONTINUE; } + if (instruction instanceof PyWithContextExitInstruction withExit && !withExit.isSuppressingExceptions(context)) { + return ControlFlowUtil.Operation.CONTINUE; + } final PsiElement element = instruction.getElement(); if (!(element instanceof PyStatement statement)) { return ControlFlowUtil.Operation.NEXT; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java index c8fbd4c9389d..a3c6aa3c8077 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -1676,6 +1676,16 @@ public final class PyTypeChecker { return null; } + @ApiStatus.Internal + public static @Nullable PyType convertToType(@Nullable PyType type, @NotNull PyClassType superType, @NotNull TypeEvalContext context) { + MatchContext matchContext = new MatchContext(context, new GenericSubstitutions(), false); + Optional matched = match(superType, type, matchContext); + if (matched.orElse(false)) { + return substitute(superType, matchContext.mySubstitutions, context); + } + return null; + } + @ApiStatus.Internal public static class Generics { private final @NotNull Set typeVars = new LinkedHashSet<>(); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java index 444b3d7b35b0..68d806585ea8 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java @@ -19,6 +19,9 @@ import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.UserDataHolder; import com.intellij.psi.PsiElement; +import com.intellij.util.ObjectUtils; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyPsiFacade; import com.jetbrains.python.psi.impl.PyBuiltinCache; import one.util.streamex.StreamEx; import org.jetbrains.annotations.ApiStatus; @@ -167,4 +170,16 @@ public final class PyTypeUtil { public static @Nullable PyType getEffectiveBound(@NotNull PyTypeVarType typeVarType) { return typeVarType.getConstraints().isEmpty() ? typeVarType.getBound() : PyUnionType.union(typeVarType.getConstraints()); } + + @ApiStatus.Internal + public static @Nullable PyType convertToType(@Nullable PyType type, + @NotNull String superTypeName, + @NotNull PsiElement anchor, + @NotNull TypeEvalContext context) { + PyClass superClass = PyPsiFacade.getInstance(anchor.getProject()).createClassByQName(superTypeName, anchor); + if (superClass == null) return null; + PyClassType superClassType = ObjectUtils.notNull(PyTypeChecker.findGenericDefinitionType(superClass, context), + new PyClassTypeImpl(superClass, false)); + return PyTypeChecker.convertToType(type, superClassType, context); + } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java b/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java index 00a7b0daac85..d468ac7ad9bd 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java @@ -24,10 +24,7 @@ import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.Version; import com.intellij.psi.PsiElement; import com.intellij.psi.util.QualifiedName; -import com.jetbrains.python.codeInsight.controlflow.CallInstruction; -import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; -import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction; -import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.codeInsight.controlflow.*; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyAugAssignmentStatementNavigator; @@ -77,6 +74,12 @@ public final class PyDefUseUtil { final HashMap pendingTypeGuard = new HashMap<>(); ControlFlowUtil.iteratePrev(startNum, instructions, instruction -> { + if (instruction instanceof PyWithContextExitInstruction withExit) { + // probably should remove acceptTypeAssertions and make context nullable + if (!withExit.isSuppressingExceptions(context)) { + return ControlFlowUtil.Operation.CONTINUE; + } + } if (acceptTypeAssertions && instruction instanceof CallInstruction callInstruction) { var typeGuardInstruction = pendingTypeGuard.get(instruction.getElement()); if (typeGuardInstruction != null) { diff --git a/python/testData/codeInsight/controlflow/Assert.txt b/python/testData/codeInsight/controlflow/Assert.txt index bfce168320d6..646b3e3ebd1a 100644 --- a/python/testData/codeInsight/controlflow/Assert.txt +++ b/python/testData/codeInsight/controlflow/Assert.txt @@ -16,5 +16,5 @@ 15(18) ASSERTTYPE ACCESS: s 16(17) element: null. Condition: isinstance(s, basestring):true 17(19) ASSERTTYPE ACCESS: s -18(19) element: null +18(19) raise: PyAssertStatement 19() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/ContinueInPositiveIteration.txt b/python/testData/codeInsight/controlflow/ContinueInPositiveIteration.txt index 48e94eb6590d..48de0160573a 100644 --- a/python/testData/codeInsight/controlflow/ContinueInPositiveIteration.txt +++ b/python/testData/codeInsight/controlflow/ContinueInPositiveIteration.txt @@ -15,7 +15,7 @@ 14(15) element: PyExpressionStatement 15(16) READ ACCESS: sys 16(4,17) element: PyCallExpression: sys.exit -17(18) element: PyRaiseStatement +17(18) raise: PyRaiseStatement 18(19) READ ACCESS: Exception 19(20) element: PyCallExpression: Exception 20() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/DoubleTry.txt b/python/testData/codeInsight/controlflow/DoubleTry.txt index 3b569d1fa0d1..ba2424a1fa75 100644 --- a/python/testData/codeInsight/controlflow/DoubleTry.txt +++ b/python/testData/codeInsight/controlflow/DoubleTry.txt @@ -5,7 +5,7 @@ 4(5,9) WRITE ACCESS: lsprof 5(6) element: PyExceptPart 6(7) READ ACCESS: ImportError -7(8) element: PyRaiseStatement +7(8) raise: PyRaiseStatement 8(25) READ ACCESS: Error 9(10) element: PyAssignmentStatement 10(11) WRITE ACCESS: p diff --git a/python/testData/codeInsight/controlflow/IfElseReturn.txt b/python/testData/codeInsight/controlflow/IfElseReturn.txt index cebff70af319..6a3ed2c902d2 100644 --- a/python/testData/codeInsight/controlflow/IfElseReturn.txt +++ b/python/testData/codeInsight/controlflow/IfElseReturn.txt @@ -11,7 +11,7 @@ 10(11,18) READ ACCESS: renderer 11(12) element: PyExceptPart 12(13) READ ACCESS: KeyError -13(14) element: PyRaiseStatement +13(14) raise: PyRaiseStatement 14(18) READ ACCESS: Error 15(16) element: PyStatementList 16(17) element: PyReturnStatement diff --git a/python/testData/codeInsight/controlflow/Raise.txt b/python/testData/codeInsight/controlflow/Raise.txt index 193aebd9d297..d14c3908eb44 100644 --- a/python/testData/codeInsight/controlflow/Raise.txt +++ b/python/testData/codeInsight/controlflow/Raise.txt @@ -1,7 +1,7 @@ 0(1) element: null 1(2) element: PyTryExceptStatement 2(3,6) element: PyTryPart -3(4,6) element: PyRaiseStatement +3(4,6) raise: PyRaiseStatement 4(5,6) READ ACCESS: Exception 5(6,13) element: PyCallExpression: Exception 6(7) element: PyExceptPart @@ -9,6 +9,6 @@ 8(9) READ ACCESS: Exception 9(10) element: PyCallExpression: Exception 10(11) WRITE ACCESS: foo -11(12) element: PyRaiseStatement +11(12) raise: PyRaiseStatement 12(13) READ ACCESS: foo 13() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/Try.txt b/python/testData/codeInsight/controlflow/Try.txt index bdf3c2aeddb4..e38e18280b9d 100644 --- a/python/testData/codeInsight/controlflow/Try.txt +++ b/python/testData/codeInsight/controlflow/Try.txt @@ -33,5 +33,5 @@ 32(33) element: PySubscriptionExpression 33(34) READ ACCESS: sys 34(35) element: PyCallExpression: sys.exc_info -35(36) element: PyRaiseStatement +35(36) raise: PyRaiseStatement 36() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TryBreak.txt b/python/testData/codeInsight/controlflow/TryBreak.txt index 5b9f9b858e3e..3017b6425fdd 100644 --- a/python/testData/codeInsight/controlflow/TryBreak.txt +++ b/python/testData/codeInsight/controlflow/TryBreak.txt @@ -5,9 +5,9 @@ 4(5,8,12) READ ACCESS: bar 5(6,8) element: PyTargetExpression: i 6(7,8) WRITE ACCESS: i -7(8,12) element: PyBreakStatement +7(12) element: PyBreakStatement 8(9) element: PyExceptPart -9(10) element: PyRaiseStatement +9(10) raise: PyRaiseStatement 10(11) READ ACCESS: Exception 11(13) element: PyCallExpression: Exception 12(13) element: PyReturnStatement diff --git a/python/testData/codeInsight/controlflow/TryRaiseFinally.txt b/python/testData/codeInsight/controlflow/TryRaiseFinally.txt index 0c1cfef8b461..0a575acaa595 100644 --- a/python/testData/codeInsight/controlflow/TryRaiseFinally.txt +++ b/python/testData/codeInsight/controlflow/TryRaiseFinally.txt @@ -1,7 +1,7 @@ 0(1) element: null 1(2) element: PyTryExceptStatement 2(3,5) element: PyTryPart -3(4,5) element: PyRaiseStatement +3(4,5) raise: PyRaiseStatement 4(5) READ ACCESS: KeyboardInterrupt 5(6) element: PyFinallyPart 6(7) element: PyPrintStatement diff --git a/python/testData/codeInsight/controlflow/TryTry.txt b/python/testData/codeInsight/controlflow/TryTry.txt index 0ef3eeb3a0b8..504c65500906 100644 --- a/python/testData/codeInsight/controlflow/TryTry.txt +++ b/python/testData/codeInsight/controlflow/TryTry.txt @@ -23,21 +23,21 @@ 22(23) element: null. Condition: x == 0:true 23(51,24) ASSERTTYPE ACCESS: x 24(25) element: PyStatementList -25(51,54) element: PyBreakStatement +25(54) element: PyBreakStatement 26(27,29,51) READ ACCESS: x 27(28) element: null. Condition: x == 1:false 28(51,33) ASSERTTYPE ACCESS: x 29(30) element: null. Condition: x == 1:true 30(51,31) ASSERTTYPE ACCESS: x 31(32) element: PyStatementList -32(7,51,54) element: PyContinueStatement +32(7,54) element: PyContinueStatement 33(34,36,51) READ ACCESS: x 34(35) element: null. Condition: x == 2:false 35(51,42) ASSERTTYPE ACCESS: x 36(37) element: null. Condition: x == 2:true 37(51,38) ASSERTTYPE ACCESS: x 38(39) element: PyStatementList -39(40,51) element: PyRaiseStatement +39(40,51) raise: PyRaiseStatement 40(41,51) READ ACCESS: Exception 41(51) element: PyCallExpression: Exception 42(43,45,51) READ ACCESS: x diff --git a/python/testData/codeInsight/controlflow/WithAssert.txt b/python/testData/codeInsight/controlflow/WithAssert.txt index f8dc858cf3ca..1777d5c4c113 100644 --- a/python/testData/codeInsight/controlflow/WithAssert.txt +++ b/python/testData/codeInsight/controlflow/WithAssert.txt @@ -1,14 +1,15 @@ 0(1) element: null 1(2) element: PyWithStatement -2(3) READ ACCESS: context_manager -3(4) element: PyAssertStatement -4(5) READ ACCESS: True -5(6) READ ACCESS: f -6(7) element: PyCallExpression: f -7(8,9) READ ACCESS: True -8(10) element: null. Condition: True:false -9(11) element: null. Condition: True:true -10(13) element: null -11(12) element: PyPrintStatement -12(13) element: PyPrintStatement -13() element: null \ No newline at end of file +2(4) READ ACCESS: context_manager +3(13) exit context manager: context_manager +4(5,3) element: PyAssertStatement +5(6,3) READ ACCESS: True +6(7,3) READ ACCESS: f +7(8,3) element: PyCallExpression: f +8(9,10,3) READ ACCESS: True +9(11) element: null. Condition: True:false +10(12) element: null. Condition: True:true +11(14,3) raise: PyAssertStatement +12(13,3) element: PyPrintStatement +13(14) element: PyPrintStatement +14() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/WithAssertFalse.txt b/python/testData/codeInsight/controlflow/WithAssertFalse.txt index e1211b6d8391..2c41d95a4997 100644 --- a/python/testData/codeInsight/controlflow/WithAssertFalse.txt +++ b/python/testData/codeInsight/controlflow/WithAssertFalse.txt @@ -1,10 +1,11 @@ 0(1) element: null 1(2) element: PyWithStatement -2(3) READ ACCESS: context_manager -3(4) element: PyAssertStatement -4(5) READ ACCESS: False -5(6) READ ACCESS: f -6(8,9) element: PyCallExpression: f -7(8) element: PyPrintStatement -8(9) element: PyPrintStatement -9() element: null \ No newline at end of file +2(4) READ ACCESS: context_manager +3(9) exit context manager: context_manager +4(5,3) element: PyAssertStatement +5(6,3) READ ACCESS: False +6(7,3) READ ACCESS: f +7(10,3) element: PyCallExpression: f +8(9,3) element: PyPrintStatement +9(10) element: PyPrintStatement +10() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/WithRaiseException.txt b/python/testData/codeInsight/controlflow/WithRaiseException.txt index 8d4d7f1806c2..54cd067bb119 100644 --- a/python/testData/codeInsight/controlflow/WithRaiseException.txt +++ b/python/testData/codeInsight/controlflow/WithRaiseException.txt @@ -1,17 +1,18 @@ 0(1) element: null 1(2) element: PyWithStatement -2(3) READ ACCESS: context_manager -3(4) element: PyIfStatement -4(5,6) READ ACCESS: c -5(11) element: null. Condition: c:false -6(7) element: null. Condition: c:true -7(8) ASSERTTYPE ACCESS: c -8(9) element: PyStatementList -9(10) element: PyRaiseStatement -10(14,16) READ ACCESS: ValueError -11(12) element: PyAssignmentStatement -12(13) READ ACCESS: c -13(14) WRITE ACCESS: val -14(15) element: PyPrintStatement -15(16) READ ACCESS: val -16() element: null \ No newline at end of file +2(4) READ ACCESS: context_manager +3(15) exit context manager: context_manager +4(5,3) element: PyIfStatement +5(6,7,3) READ ACCESS: c +6(12) element: null. Condition: c:false +7(8) element: null. Condition: c:true +8(3,9) ASSERTTYPE ACCESS: c +9(10) element: PyStatementList +10(11,3) raise: PyRaiseStatement +11(17,3) READ ACCESS: ValueError +12(13,3) element: PyAssignmentStatement +13(14,3) READ ACCESS: c +14(15,3) WRITE ACCESS: val +15(16) element: PyPrintStatement +16(17) READ ACCESS: val +17() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/WithSeveralContextsAssert.py b/python/testData/codeInsight/controlflow/WithSeveralContextsAssert.py new file mode 100644 index 000000000000..4edcd2b5d4cb --- /dev/null +++ b/python/testData/codeInsight/controlflow/WithSeveralContextsAssert.py @@ -0,0 +1,6 @@ + +with cm1, cm2, cm3: + assert False, f() + print("Unreachable") + +print("Reachable") \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/WithSeveralContextsAssert.txt b/python/testData/codeInsight/controlflow/WithSeveralContextsAssert.txt new file mode 100644 index 000000000000..eb813c67677d --- /dev/null +++ b/python/testData/codeInsight/controlflow/WithSeveralContextsAssert.txt @@ -0,0 +1,15 @@ +0(1) element: null +1(2) element: PyWithStatement +2(4) READ ACCESS: cm1 +3(13) exit context manager: cm1 +4(3,6) READ ACCESS: cm2 +5(13) exit context manager: cm2 +6(3,5,8) READ ACCESS: cm3 +7(13) exit context manager: cm3 +8(9,3,5,7) element: PyAssertStatement +9(10,3,5,7) READ ACCESS: False +10(11,3,5,7) READ ACCESS: f +11(14,3,5,7) element: PyCallExpression: f +12(13,3,5,7) element: PyPrintStatement +13(14) element: PyPrintStatement +14() element: null \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py index a0cbfca4f566..30e2ab7ddda1 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py +++ b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py @@ -17,7 +17,7 @@ def test(c): """ x1 = f1(c) f2(x1) # Weaker union types - f3(x1) + f3(x1) f2(x1.count('')) f3(x1.count('')) diff --git a/python/testData/inspections/PyUnreachableCodeInspection/Unreachable.py b/python/testData/inspections/PyUnreachableCodeInspection/Unreachable.py index 20120935cb7e..38e0f79e78d7 100644 --- a/python/testData/inspections/PyUnreachableCodeInspection/Unreachable.py +++ b/python/testData/inspections/PyUnreachableCodeInspection/Unreachable.py @@ -1,3 +1,5 @@ +import unittest + def f(): return a = 1 diff --git a/python/testData/inspections/PyUnreachableCodeInspection/WithSuppressedExceptions.py b/python/testData/inspections/PyUnreachableCodeInspection/WithSuppressedExceptions.py deleted file mode 100644 index b723a81a0dac..000000000000 --- a/python/testData/inspections/PyUnreachableCodeInspection/WithSuppressedExceptions.py +++ /dev/null @@ -1,58 +0,0 @@ -class C(object): - def __enter__(self): - return self - - def __exit__(self, exc, value, traceback): - return True - - -def f11(): - with C(): - raise Exception() - print(1) #pass - - -def g2(): - raise Exception() - - -def f12(): - with C(): - return g2() - print(1) #pass - -def f13(): - with C(): - assert False - print(1) #pass - - -class A1(TestCase): - def f3(self): - with C(): - g2() - print(1) #pass - - -import contextlib -from contextlib import suppress -from unittest import TestCase - - -def f21(): - with suppress(Exception): - raise Exception() - print(1) #pass - - -def f22(): - with contextlib.suppress(Exception): - return g2() - print(1) #pass - - -class A2(TestCase): - def f3(self): - with self.assertRaises(Exception): - g2() - print(1) #pass \ 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 7bf3b06498b6..2f76ce7942a2 100644 --- a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java +++ b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java @@ -302,6 +302,11 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase { doTest(); } + // PY-51564 + public void testWithSeveralContextsAssert() { + doTest(); + } + // PY-29767 public void testContinueInPositiveIteration() { doTest(); diff --git a/python/testSrc/com/jetbrains/python/PyTypeConversionTest.java b/python/testSrc/com/jetbrains/python/PyTypeConversionTest.java new file mode 100644 index 000000000000..9fc6c4dca003 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/PyTypeConversionTest.java @@ -0,0 +1,74 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python; + +import com.jetbrains.python.documentation.PythonDocumentationProvider; +import com.jetbrains.python.fixtures.PyTestCase; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.types.PyClassType; +import com.jetbrains.python.psi.types.PyType; +import com.jetbrains.python.psi.types.PyTypeUtil; +import com.jetbrains.python.psi.types.TypeEvalContext; +import org.jetbrains.annotations.NotNull; + +public final class PyTypeConversionTest extends PyTestCase { + public void testCustomIterableClassToTypingIterable() { + doTest("typing.Iterable", "Iterable[str]", """ + from typing import Iterator + + class Class: + def __next__(self) -> str: + return "foo" + def __iter__(self) -> Iterator[str]: + return self + + expr = Class() + """); + } + + public void testDictToTypingMapping() { + doTest("typing.Mapping", "Mapping[str, int]", """ + expr = {"foo": 42} + """); + } + + public void testTupleToTypingIterable() { + doTest("typing.Iterable", "Iterable[int | str]", """ + expr = (1, "foo") + """); + } + + public void testCustomContextManagerClassToContextlibAbstractContextManager() { + doTest("contextlib.AbstractContextManager", "AbstractContextManager[int, bool | None]", """ + class CustomManager: + def __enter__(self) -> int: + return 42 + + def __exit__(self, exc_type, exc_val, exc_tb) -> bool | None: + pass + + expr = CustomManager() + """); + } + + public void testContextManagerGeneratorToContextlibAbstractContextManager() { + doTest("contextlib.AbstractContextManager", "AbstractContextManager[str, bool | None]", """ + import contextlib + + @contextlib.contextmanager + def f(): + yield "foo" + + expr = f() + """); + } + + public void doTest(@NotNull String superTypeFqn, @NotNull String expectedResultType, @NotNull String text) { + myFixture.configureByText(PythonFileType.INSTANCE, text); + PyExpression expr = myFixture.findElementByText("expr", PyExpression.class); + TypeEvalContext context = TypeEvalContext.codeAnalysis(expr.getProject(), expr.getContainingFile()); + PyType classType = assertInstanceOf(context.getType(expr), PyClassType.class); + PyType converted = PyTypeUtil.convertToType(classType, superTypeFqn, expr, context); + String actualType = PythonDocumentationProvider.getTypeName(converted, context); + assertEquals(expectedResultType, actualType); + } +} diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java index 19f7328a8bb2..6e62cb7a0f03 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnreachableCodeInspectionTest.java @@ -25,42 +25,146 @@ public class PyUnreachableCodeInspectionTest extends PyInspectionTestCase { runWithLanguageLevel(LanguageLevel.PYTHON26, () -> doTest()); } - // PY-7420, PY-16419, PY-26417 - public void testWithSuppressedExceptions() { - doTest(); + // PY-51564 + public void testWithNotContext() { + doTestByText(""" +class NotContext: + pass + +def no_context(): + with NotContext(): + raise ValueError("Something went wrong") + print("unreachable") + """); } - // PY-7420, PY-16419, PY-26417 - public void testWithNotSuppressedExceptions() { + // PY-51564 + public void testWithContextlibUnittest() { + doTestByText(""" +import contextlib +from contextlib import suppress +from unittest import TestCase + +def cl(): + with suppress(Exception): + raise ValueError("Something went wrong") + print("reachable") + +class A(TestCase): + def f(self): + with self.assertRaises(Exception): + raise ValueError("Something went wrong") + print("reachable") + """); + } + + // PY-51564 + public void testWith() { doTestByText( """ - class C(object): - def __enter__(self): - return self +class Suppress: + def __enter__(self): ... + def __exit__(self, exc_type, exc_value, traceback) -> bool: ... - def __exit__(self, exc, value, traceback): - return False +class NoSuppress: + def __enter__(self): ... + def __exit__(self, exc_type, exc_value, traceback) -> bool | None: ... - def f1(): - with C(): - raise Exception() - print(1) #pass +def sup(b): + with Suppress(): + a = 42 + raise ValueError("Something went wrong") + print("reachable") + + with Suppress(): + assert b + a = 42 + assert False + print("reachable") - def g2(): - raise Exception() - - def f2(): - with C(): - return g2() - print(1) #pass - - def f3(): - with C(): - g2() - print(1) #pass""" +def nosup(b): + with NoSuppress(): + a = 42 + raise ValueError("Something went wrong") + print("unreachable") + + with NoSuppress(): + assert b + a = 42 + assert False + print("unreachable") + """ ); } + // PY-51564 + public void testMiltipleWith() { + doTestByText(""" +import contextlib + +@contextlib.contextmanager +def raising_exception_in_enter(p): + if p: + raise Exception + yield + + +def f(): + with contextlib.suppress(Exception): + return + print("Unreachable") + +def f2(p): + with contextlib.suppress(Exception): + with raising_exception_in_enter(p): + return + print("Reachable") + + +def f3(p): + with contextlib.suppress(Exception), raising_exception_in_enter(p): + return + print("Reachable") + """); + } + + // PY-51564 + public void testAsyncWith() { + doTestByText(""" +class AsyncSuppress: + async def __aenter__(self): ... + async def __aexit__(self, exc_type, exc_value, traceback) -> bool: ... + +class AsyncNoSuppress: + async def __aenter__(self): ... + async def __aexit__(self, exc_type, exc_value, traceback) -> bool | None: ... + +async def sup(b): + async with AsyncSuppress(): + a = 42 + raise ValueError("Something went wrong") + print("reachable") + + async with AsyncSuppress(): + assert b + a = 42 + assert False + print("reachable") + +async def nosup(b): + async with AsyncNoSuppress(): + a = 42 + raise ValueError("Something went wrong") + print("unreachable") + + async with AsyncNoSuppress(): + assert b + a = 42 + assert False + print("unreachable") + """); + } + // PY-25974 public void testExprOrSysExitAssignedToVar() { doTest(); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnusedLocalInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnusedLocalInspectionTest.java index 6865673b804f..8233b7f564a7 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnusedLocalInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnusedLocalInspectionTest.java @@ -149,7 +149,7 @@ public class PyUnusedLocalInspectionTest extends PyInspectionTestCase { def f12(): with C(): - x = 2 + x = 2 return g2() print(x) #pass