PY-79910 Variable incorrectly marked as unused or redeclared without usage in nested try/if blocks

When there is no `finally` in try-except statement, use transparent exit instruction to tie all normal exits from try-, else- and except- parts to the next instruction


Merge-request: IJ-MR-158265
Merged-by: Aleksandr Govenko <aleksandr.govenko@jetbrains.com>

GitOrigin-RevId: 734581b732a1a558b72811fdda977c470d045cc9
This commit is contained in:
Aleksandr.Govenko
2025-04-23 22:39:58 +00:00
committed by intellij-monorepo-bot
parent 89191fbef6
commit df545f8047
5 changed files with 83 additions and 28 deletions

View File

@@ -751,21 +751,18 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
final Instruction finallyFailInstruction;
// Store pending normal exit instructions from try-except-else parts
if (finallyPart != null) {
myBuilder.processPending((pendingScope, instruction) -> {
final PsiElement pendingElement = instruction.getElement();
if (pendingElement != null) {
final boolean isPending = PsiTreeUtil.isAncestor(node, pendingElement, false) &&
!PsiTreeUtil.isAncestor(finallyPart, pendingElement, false);
if (isPending && pendingScope != null) {
pendingNormalExits.add(Pair.createNonNull(pendingScope, instruction));
}
else {
myBuilder.addPendingEdge(pendingScope, instruction);
}
}
});
}
myBuilder.processPending((pendingScope, instruction) -> {
final PsiElement pendingElement = instruction.getElement();
final boolean isPending = pendingElement == null ||
PsiTreeUtil.isAncestor(node, pendingElement, false) &&
!PsiTreeUtil.isAncestor(finallyPart, pendingElement, false);
if (isPending && pendingScope != null) {
pendingNormalExits.add(Pair.createNonNull(pendingScope, instruction));
}
else {
myBuilder.addPendingEdge(pendingScope, instruction);
}
});
// Finally-fail part handling
if (finallyPart != null) {
@@ -809,6 +806,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
}
}
final Instruction exitInstruction;
if (finallyPart != null) {
myBuilder.processPending((pendingScope, instruction) -> {
final PsiElement e = instruction.getElement();
@@ -829,7 +827,6 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
// Duplicate CFG for finally (-fail and -success) only if there are some successful exits from the
// try part. Otherwise, a single CFG for finally provides the correct control flow
final Instruction finallyInstruction;
if (!pendingNormalExits.isEmpty()) {
// Finally-success part handling
pendingBackup = new ArrayList<>(myBuilder.pending);
@@ -840,24 +837,28 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
for (Pair<PsiElement, Instruction> pair : pendingBackup) {
myBuilder.addPendingEdge(pair.first, pair.second);
}
finallyInstruction = finallySuccessInstruction;
exitInstruction = finallySuccessInstruction;
}
else {
finallyInstruction = finallyFailInstruction;
exitInstruction = finallyFailInstruction;
}
}
else {
exitInstruction = addTransparentInstruction();
myBuilder.prevInstruction = exitInstruction;
}
// Connect normal exits from try and else parts to the finally part
for (Pair<PsiElement, Instruction> pendingScopeAndInstruction : pendingNormalExits) {
final PsiElement pendingScope = pendingScopeAndInstruction.first;
final Instruction instruction = pendingScopeAndInstruction.second;
// Connect normal exits from try and else parts to the finally part or exit instruction
for (Pair<PsiElement, Instruction> pendingScopeAndInstruction : pendingNormalExits) {
final PsiElement pendingScope = pendingScopeAndInstruction.first;
final Instruction instruction = pendingScopeAndInstruction.second;
myBuilder.addEdge(instruction, finallyInstruction);
myBuilder.addEdge(instruction, exitInstruction);
// When instruction continues outside try-except statement scope
// the last instruction in finally-block is marked as pointing to that continuation
if (PsiTreeUtil.isAncestor(pendingScope, node, true)) {
myBuilder.addPendingEdge(pendingScope, myBuilder.prevInstruction);
}
// When instruction continues outside try-except statement scope
// the last instruction in finally-block is marked as pointing to that continuation
if (PsiTreeUtil.isAncestor(pendingScope, node, true)) {
myBuilder.addPendingEdge(pendingScope, myBuilder.prevInstruction);
}
}
}

View File

@@ -0,0 +1,11 @@
try:
value = 42
except ValueError:
value = 13
except SomethingElse:
value = 1342
raise
else:
value = 0
print(value)

View File

@@ -0,0 +1,20 @@
0(1) element: null
1(2) element: PyTryExceptStatement
2(3,8,12) element: PyTryPart
3(4,8,12) element: PyAssignmentStatement
4(5,8,12) WRITE ACCESS: value
5(6) element: PyElsePart
6(7) element: PyAssignmentStatement
7(17) WRITE ACCESS: value
8(9) element: PyExceptPart
9(10) READ ACCESS: ValueError
10(11) element: PyAssignmentStatement
11(17) WRITE ACCESS: value
12(13) element: PyExceptPart
13(14) READ ACCESS: SomethingElse
14(15) element: PyAssignmentStatement
15(16) WRITE ACCESS: value
16(19) raise: PyRaiseStatement
17(18) element: PyPrintStatement
18(19) READ ACCESS: value
19() element: null

View File

@@ -578,6 +578,11 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase {
doTest();
}
// PY-79910
public void testTryExceptNoFinally() {
doTest();
}
private void doTestFirstStatement() {
final String testName = getTestName(false);
configureByFile(testName + ".py");

View File

@@ -126,6 +126,24 @@ public class PyUnusedLocalInspectionTest extends PyInspectionTestCase {
inspection.ignoreTupleUnpacking = false;
doTest(inspection);
}
// PY-79910
public void testTryExceptInsideIfInsideFunction() {
doTestByText("""
def test():
num = 7
if num < 10:
try:
next_num = input() # used
except ValueError:
next_num = None
else:
next_num = 0
return next_num
"""
);
}
// PY-16419, PY-26417
public void testPotentiallySuppressedExceptions() {