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 <aleksandr.govenko@jetbrains.com>

GitOrigin-RevId: fd5a139de31d47136cc86b4a7e090a20b73b8744
This commit is contained in:
Aleksandr.Govenko
2025-02-14 13:18:20 +00:00
committed by intellij-monorepo-bot
parent cb97edfcf7
commit 6659947437
34 changed files with 477 additions and 262 deletions
@@ -24,10 +24,4 @@ class CallInstruction(builder: ControlFlowBuilder, call: PyCallExpression) : Ins
}
return false
}
companion object {
fun allPredWithoutNoReturn(instruction: Instruction, typeEvalContext: TypeEvalContext): List<Instruction> {
return instruction.allPred().filter { it !is CallInstruction || !it.isNoReturnCall(typeEvalContext) }
}
}
}
@@ -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<String> 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<Instruction> 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) {
@@ -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)
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}"
}
@@ -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 }
}
}
@@ -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<String> TYPE_DICT_QUALIFIERS = Set.of(REQUIRED, REQUIRED_EXT, NOT_REQUIRED, NOT_REQUIRED_EXT, READONLY, READONLY_EXT);
public static final String UNPACK = "typing.Unpack";
@@ -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<Boolean> 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<PsiElement> collectUnreachable(@NotNull ScopeOwner owner, @Nullable PsiElement anchor, @NotNull TypeEvalContext context) {
final ControlFlow flow = ControlFlowCache.getControlFlow(owner);
final Instruction[] instructions = flow.getInstructions();
final List<PsiElement> 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<Instruction> 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() {
@@ -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;
@@ -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<PsiElement> 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;
}
}
}
@@ -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;
@@ -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<PyFunctionStub> 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;
@@ -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<Boolean> 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<PyTypeVarType> typeVars = new LinkedHashSet<>();
@@ -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);
}
}
@@ -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<PyCallSiteExpression, ConditionalInstruction> 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) {
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
@@ -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
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
@@ -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
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
@@ -0,0 +1,6 @@
with cm1, cm2, cm3:
assert False, f()
print("Unreachable")
print("Reachable")
@@ -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
@@ -17,7 +17,7 @@ def test(c):
"""
x1 = f1(c)
f2(x1) # Weaker union types
f3(<warning descr="Expected type 'int', got 'Union[str, list, None]' instead">x1</warning>)
f3(<warning descr="Expected type 'int', got 'Union[list, str, None]' instead">x1</warning>)
f2(<warning descr="Expected type 'str', got 'int' instead">x1.count('')</warning>)
f3(x1.count(''))
@@ -1,3 +1,5 @@
import unittest
def f():
return
<warning descr="This code is unreachable">a = 1</warning>
@@ -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()
<warning descr="This code is unreachable">print(1) #pass</warning>
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
@@ -302,6 +302,11 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase {
doTest();
}
// PY-51564
public void testWithSeveralContextsAssert() {
doTest();
}
// PY-29767
public void testContinueInPositiveIteration() {
doTest();
@@ -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);
}
}
@@ -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")
<warning descr="This code is unreachable">print("unreachable")</warning>
""");
}
// 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()
<warning descr="This code is unreachable">print(1) #pass</warning>
def f3():
with C():
g2()
print(1) #pass"""
def nosup(b):
with NoSuppress():
a = 42
raise ValueError("Something went wrong")
<warning descr="This code is unreachable">print("unreachable")</warning>
with NoSuppress():
assert b
a = 42
assert False
<warning descr="This code is unreachable">print("unreachable")</warning>
"""
);
}
// 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
<warning descr="This code is unreachable">print("Unreachable")</warning>
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")
<warning descr="This code is unreachable">print("unreachable")</warning>
async with AsyncNoSuppress():
assert b
a = 42
assert False
<warning descr="This code is unreachable">print("unreachable")</warning>
""");
}
// PY-25974
public void testExprOrSysExitAssignedToVar() {
doTest();
@@ -149,7 +149,7 @@ public class PyUnusedLocalInspectionTest extends PyInspectionTestCase {
def f12():
with C():
<weak_warning descr="Local variable 'x' value is not used">x</weak_warning> = 2
x = 2
return g2()
print(x) #pass