PY-81676 Don't report lines with assert_never as unreachable

(cherry picked from commit afdbf35915823de02a6b8551f57770113d0feb2f)

IJ-CR-172556

GitOrigin-RevId: 0f8526e2877736ac606db9ed4657a530a66d6f23
This commit is contained in:
Aleksandr.Govenko
2025-06-13 20:56:01 +02:00
committed by intellij-monorepo-bot
parent 5a60b17b4d
commit adcc559e5c
47 changed files with 652 additions and 477 deletions

View File

@@ -23,6 +23,7 @@ import com.intellij.codeInsight.controlflow.TransparentInstruction;
import com.intellij.codeInsight.controlflow.impl.ConditionalInstructionImpl;
import com.intellij.codeInsight.controlflow.impl.TransparentInstructionImpl;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.util.PsiTreeUtil;
@@ -32,6 +33,7 @@ import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.*;
import com.jetbrains.python.psi.types.PyNeverType;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -44,6 +46,9 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
private final ControlFlowBuilder myBuilder = new ControlFlowBuilder();
private @Nullable TrueFalseNodes myTrueFalseNodes;
// see com.jetbrains.python.PyPatternTypeTest.testMatchClassPatternShadowingCapture
private final @NotNull List<String> myPatternBindingNames = new ArrayList<>();
private record TrueFalseNodes(@NotNull Instruction trueNode, @NotNull Instruction falseNode) {}
@@ -334,12 +339,46 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
public void visitPyMatchStatement(@NotNull PyMatchStatement matchStatement) {
myBuilder.startNode(matchStatement);
PyExpression subject = matchStatement.getSubject();
String subjectName = PyTypeAssertionEvaluator.getAssertionTargetName(subject);
if (subject != null) {
subject.accept(this);
}
for (PyCaseClause caseClause : matchStatement.getCaseClauses()) {
visitPyCaseClause(caseClause);
Instruction nextClause = myBuilder.prevInstruction;
boolean unreachable = false;
for (PyCaseClause clause : matchStatement.getCaseClauses()) {
myBuilder.prevInstruction = nextClause;
nextClause = addTransparentInstruction();
myPatternBindingNames.clear();
PyPattern pattern = clause.getPattern();
if (pattern != null) {
pattern.accept(this);
if (!myPatternBindingNames.contains(subjectName)) {
addTypeAssertionNodes(clause, true);
}
}
PyExpression guard = clause.getGuardCondition();
if (guard != null) {
TransparentInstruction trueNode = addTransparentInstruction();
visitCondition(guard, trueNode, nextClause);
myBuilder.prevInstruction = trueNode;
addTypeAssertionNodes(guard, true);
}
if (unreachable) {
addAssertTypeNever();
}
if (pattern != null && pattern.isIrrefutable()) {
unreachable = true;
}
myBuilder.startNode(clause.getStatementList());
clause.getStatementList().accept(this);
myBuilder.addPendingEdge(matchStatement, myBuilder.prevInstruction);
myBuilder.updatePendingElementScope(clause.getStatementList(), matchStatement);
}
myBuilder.prevInstruction = nextClause;
myBuilder.addNodeAndCheckPending(new TransparentInstructionImpl(myBuilder, matchStatement, ""));
if (!myBuilder.prevInstruction.allPred().isEmpty()) {
addTypeAssertionNodes(matchStatement, false);
@@ -348,53 +387,20 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
myBuilder.prevInstruction = null;
}
@Override
public void visitPyCaseClause(@NotNull PyCaseClause clause) {
PyPattern pattern = clause.getPattern();
if (pattern != null) {
pattern.accept(this);
}
TransparentInstruction trueNode = addTransparentInstruction();
TransparentInstruction falseNode = addTransparentInstruction();
PyExpression guard = clause.getGuardCondition();
if (guard != null) {
visitCondition(guard, trueNode, falseNode);
addTypeAssertionNodes(guard, true);
myBuilder.addPendingEdge(clause, falseNode);
}
else {
myBuilder.addEdge(myBuilder.prevInstruction, trueNode);
}
myBuilder.prevInstruction = trueNode;
clause.getStatementList().accept(this);
if (clause.getParent() instanceof PyMatchStatement matchStatement) {
myBuilder.addPendingEdge(matchStatement, myBuilder.prevInstruction);
myBuilder.updatePendingElementScope(clause.getStatementList(), matchStatement);
}
myBuilder.prevInstruction = null;
}
@Override
public void visitWildcardPattern(@NotNull PyWildcardPattern node) {
myBuilder.startNode(node);
addTypeAssertionNodes(node, true);
}
@Override
public void visitPyPattern(@NotNull PyPattern node) {
boolean isRefutable = !node.isIrrefutable();
if (isRefutable) {
myBuilder.addNodeAndCheckPending(new RefutablePatternInstruction(myBuilder, node, false));
myBuilder.addPendingEdge(node.getParent(), myBuilder.prevInstruction);
}
else {
myBuilder.startNode(node);
}
myBuilder.addPendingEdge(node.getParent(), myBuilder.prevInstruction);
node.acceptChildren(this);
myBuilder.updatePendingElementScope(node, node.getParent());
addTypeAssertionNodes(node, true);
if (isRefutable) {
myBuilder.addNode(new RefutablePatternInstruction(myBuilder, node, true));
}
@@ -429,7 +435,6 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
node.getClassNameReference().accept(this);
myBuilder.addPendingEdge(node.getParent(), myBuilder.prevInstruction);
addTypeAssertionNodes(node, true);
node.getArgumentList().acceptChildren(this);
myBuilder.updatePendingElementScope(node, node.getParent());
@@ -443,7 +448,6 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
node.getValue().accept(this);
myBuilder.addPendingEdge(node.getParent(), myBuilder.prevInstruction);
addTypeAssertionNodes(node, true);
myBuilder.addNode(new RefutablePatternInstruction(myBuilder, node, true));
}
@@ -453,9 +457,20 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
// So no need to create an additional fail edge
myBuilder.startNode(node);
node.acceptChildren(this);
myPatternBindingNames.add(node.getTarget().getName());
myBuilder.updatePendingElementScope(node, node.getParent());
}
@Override
public void visitPyCapturePattern(@NotNull PyCapturePattern node) {
node.acceptChildren(this);
// Although capture pattern is irrefutable, I add fail edge
// here to add some connection to the next case clause.
// Perhaps this can be reworked and simplified later.
myBuilder.addPendingEdge(node.getParent(), myBuilder.prevInstruction);
myPatternBindingNames.add(node.getTarget().getName());
}
@Override
public void visitPyGroupPattern(@NotNull PyGroupPattern node) {
// GroupPattern can't fail by itself it fails only if its child fails.
@@ -478,13 +493,16 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
if (condition != null) {
visitCondition(condition, thenNode, elseNode);
}
myBuilder.prevInstruction = thenNode;
Boolean conditionResult = PyEvaluator.evaluateAsBooleanNoResolve(condition);
myBuilder.prevInstruction = unreachable || Boolean.FALSE.equals(conditionResult) ? null : thenNode;
if (unreachable || Boolean.FALSE.equals(conditionResult)) {
// Condition is always False, or some previous condition is always True.
addAssertTypeNever();
}
if (Boolean.TRUE.equals(conditionResult)) {
unreachable = true;
}
visitPyStatementPart(ifPart);
exitInstructions.add(myBuilder.prevInstruction);
@@ -494,7 +512,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
final PyElsePart elsePart = node.getElsePart();
if (elsePart != null) {
if (unreachable) {
myBuilder.prevInstruction = null;
addAssertTypeNever();
}
visitPyStatementPart(elsePart);
}
@@ -1124,6 +1142,13 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
return instruction;
}
/**
* Can be used to mark a branch as unreachable.
*/
private void addAssertTypeNever() {
myBuilder.addNode(ReadWriteInstruction.assertType(myBuilder, null, null, context -> Ref.create(PyNeverType.NEVER)));
}
/**
* Can be used to collect all pending edges
* that we used to build CFG for `node`,

View File

@@ -5,6 +5,7 @@ import com.intellij.codeInsight.controlflow.Instruction
import com.intellij.openapi.util.Version
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.findParentOfType
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyEvaluator
@@ -88,7 +89,16 @@ class PyDataFlow(scopeOwner: ScopeOwner, controlFlow: ControlFlow, private val c
* - calls to functions annotated with `NoReturn`
*/
fun PsiElement.isUnreachableForInspection(context: TypeEvalContext): Boolean {
return isUnreachableByControlFlow(context) && !isFirstTerminatingStatement(context)
return PyUtil.getParameterizedCachedValue(this, context) { isUnreachableForInspectionNoCache(it) }
}
private fun PsiElement.isUnreachableForInspectionNoCache(context: TypeEvalContext): Boolean {
if (parent is PyElement && parent.isUnreachableForInspection(context)) return true
return isUnreachableByControlFlow(context) && when (this) {
is PyStatementList -> !(statements.firstOrNull()?.isIgnoredUnreachableStatement(context) ?: true)
is PyStatement -> !this.isIgnoredUnreachableStatement(context)
else -> false
}
}
/**
@@ -128,12 +138,11 @@ fun PsiElement.findInstructionNumber(flow: Array<Instruction>): Int {
return -1
}
private fun PsiElement.isFirstTerminatingStatement(context: TypeEvalContext): Boolean {
if (this.isTerminatingStatement(context)) {
val prevSibling = prevSiblingOfType<PyElement>() ?: return true
return !prevSibling.isTerminatingStatement(context) && !prevSibling.isUnreachableByControlFlow(context)
}
return false
private fun PyStatement.isIgnoredUnreachableStatement(context: TypeEvalContext): Boolean {
val parentBlock = this.parent as? PyStatementList ?: return false
if (parentBlock.statements[0] != this) return false
val parentCompoundStatement = parentBlock.findParentOfType<PyStatement>() ?: return false
return !parentCompoundStatement.isUnreachableByControlFlow(context) && isTerminatingStatement(context)
}
private fun PsiElement.isTerminatingStatement(context: TypeEvalContext): Boolean {

View File

@@ -168,10 +168,11 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
}
@Override
public void visitPyPattern(@NotNull PyPattern node) {
final PsiElement parent = PsiTreeUtil.skipParentsOfType(node, PyCaseClause.class, PyGroupPattern.class, PyAsPattern.class, PyOrPattern.class);
if (parent instanceof PyMatchStatement matchStatement) {
pushAssertion(matchStatement.getSubject(), myPositive, context -> context.getType(node));
public void visitPyCaseClause(@NotNull PyCaseClause node) {
var pattern = node.getPattern();
if (pattern == null) return;
if (node.getParent() instanceof PyMatchStatement matchStatement) {
pushAssertion(matchStatement.getSubject(), myPositive, context -> context.getType(pattern));
}
}
@@ -189,6 +190,10 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
for (PyCaseClause cs : matchStatement.getCaseClauses()) {
if (cs.getPattern() == null) continue;
if (cs.getGuardCondition() != null) continue;
if (cs.getPattern().isIrrefutable()) {
subjectType = PyNeverType.NEVER;
break;
}
subjectType = Ref.deref(createAssertionType(subjectType, context.getType(cs.getPattern()), false, context));
}
@@ -358,6 +363,19 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
}
}
public static @Nullable String getAssertionTargetName(@Nullable PyExpression expression) {
PyExpression target = PyPsiUtils.flattenParens(expression);
if (target instanceof PyAssignmentExpression walrus) {
return getAssertionTargetName(walrus.getTarget());
}
if (target instanceof PyReferenceExpression || target instanceof PyTargetExpression) {
if (!((PyQualifiedExpression)target).isQualified()) {
return target.getName();
}
}
return null;
}
private static boolean isIfReferenceStatement(@NotNull PyExpression node) {
return PsiTreeUtil.skipParentsOfType(node, PyParenthesizedExpression.class) instanceof PyIfPart;
}
@@ -392,4 +410,4 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
return myFunction;
}
}
}
}

View File

@@ -7,7 +7,7 @@ import com.intellij.codeInsight.dataflow.map.DfaMapInstance;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.codeInsight.controlflow.CallInstruction;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.codeInsight.controlflow.PyDataFlowKt;
import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeVariable;
@@ -37,11 +37,8 @@ public class PyReachingDefsDfaInstance implements DfaMapInstance<ScopeVariable>
if (element == null || ((PyFile) element.getContainingFile()).getLanguageLevel().isPython2()){
return processReducedMap(map, instruction, element);
}
var scope = ScopeUtil.getScopeOwner(element);
if (scope != null) {
if (ControlFlowCache.getDataFlow(scope, myContext).isUnreachable(instruction)) {
return UNREACHABLE_MARKER;
}
if (PyDataFlowKt.isUnreachableForInspection(element, myContext)) {
return UNREACHABLE_MARKER;
}
if (instruction instanceof CallInstruction callInstruction) {
if (callInstruction.isNoReturnCall(myContext)) {

View File

@@ -5,13 +5,13 @@ import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.findParentInFile
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.controlflow.isUnreachableForInspection
import com.jetbrains.python.psi.PyElement
import com.jetbrains.python.psi.PyStatement
import com.jetbrains.python.psi.PyStatementList
/**
* Detects unreachable code using control flow graph
* Detects unreachable code using the control flow graph
*/
class PyUnreachableCodeInspection : PyInspection() {
override fun buildVisitor(
@@ -20,12 +20,16 @@ class PyUnreachableCodeInspection : PyInspection() {
session: LocalInspectionToolSession
): PsiElementVisitor {
return object : PyInspectionVisitor(holder, getContext(session)) {
override fun visitPyElement(node: PyElement) {
override fun visitPyStatementList(node: PyStatementList) {
if (node.parent.isUnreachableForInspection(myTypeEvalContext)) return
if (node.isUnreachableForInspection(myTypeEvalContext)) {
registerProblem(node, PyPsiBundle.message("INSP.unreachable.code"), ProblemHighlightType.LIKE_UNUSED_SYMBOL)
}
}
override fun visitPyStatement(node: PyStatement) {
if (node.parent.isUnreachableForInspection(myTypeEvalContext)) return
if (node.isUnreachableForInspection(myTypeEvalContext)) {
if (node.findParentInFile { it.isUnreachableForInspection(myTypeEvalContext) } != null) {
// We only want to highlight top level unreachable code
return
}
registerProblem(node, PyPsiBundle.message("INSP.unreachable.code"), ProblemHighlightType.LIKE_UNUSED_SYMBOL)
}
}