[pycharm] add un-reachability support to PyUnboundLocalVariableInspection

GitOrigin-RevId: ea1125977ea8b4de370973d606cc5ba7b258dd82
This commit is contained in:
Vladimir Koshelev
2024-07-10 14:24:54 +02:00
committed by intellij-monorepo-bot
parent 582edda2e8
commit d649ae4e61
8 changed files with 92 additions and 12 deletions

View File

@@ -6,12 +6,14 @@ import com.intellij.codeInsight.dataflow.map.DFAMap;
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.ReadWriteInstruction;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeVariable;
import com.jetbrains.python.codeInsight.dataflow.scope.impl.ScopeVariableImpl;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyExceptPartNavigator;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
@@ -19,13 +21,26 @@ import java.util.Map;
public class PyReachingDefsDfaInstance implements DfaMapInstance<ScopeVariable> {
// Use this its own map, because check in PyReachingDefsDfaSemilattice is important
public static final DFAMap<ScopeVariable> INITIAL_MAP = new DFAMap<>();
public static final DFAMap<ScopeVariable> UNREACHABLE_MARKER = new DFAMap<>();
private final TypeEvalContext myContext;
public PyReachingDefsDfaInstance(@NotNull TypeEvalContext typeEvalContext) {
myContext = typeEvalContext;
}
@Override
public DFAMap<ScopeVariable> fun(final DFAMap<ScopeVariable> map, final Instruction instruction) {
if (map == UNREACHABLE_MARKER) return map;
final PsiElement element = instruction.getElement();
if (element == null || ((PyFile) element.getContainingFile()).getLanguageLevel().isPython2()){
return processReducedMap(map, instruction, element);
}
if (instruction instanceof CallInstruction callInstruction) {
if (callInstruction.isNoReturnCall(myContext)) {
return UNREACHABLE_MARKER;
}
}
// Scope reduction
final DFAMap<ScopeVariable> reducedMap = new DFAMap<>();
for (Map.Entry<String, ScopeVariable> entry : map.entrySet()) {

View File

@@ -16,6 +16,7 @@ import java.util.Set;
public class PyReachingDefsSemilattice implements MapSemilattice<ScopeVariable> {
@Override
public boolean eq(@NotNull DFAMap<ScopeVariable> e1, @NotNull DFAMap<ScopeVariable> e2) {
if (e1 == PyReachingDefsDfaInstance.UNREACHABLE_MARKER || e2 == PyReachingDefsDfaInstance.UNREACHABLE_MARKER) return e1 == e2;
if (e1 == PyReachingDefsDfaInstance.INITIAL_MAP && e2 != PyReachingDefsDfaInstance.INITIAL_MAP ||
e2 == PyReachingDefsDfaInstance.INITIAL_MAP && e1 != PyReachingDefsDfaInstance.INITIAL_MAP) {
return false;
@@ -32,6 +33,8 @@ public class PyReachingDefsSemilattice implements MapSemilattice<ScopeVariable>
return ins.get(0);
}
ins = ins.stream().filter( e -> e != PyReachingDefsDfaInstance.UNREACHABLE_MARKER).toList();
final Set<String> resultNames = getResultNames(ins);
if (resultNames == null || resultNames.isEmpty()) {
return new DFAMap<>();

View File

@@ -20,6 +20,8 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNamedElement;
import com.jetbrains.python.psi.PyImportedNameDefiner;
import com.jetbrains.python.psi.PyTargetExpression;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -29,9 +31,15 @@ import java.util.List;
public interface Scope {
/*
* @return defined scope local/instance/class variables and parameters, using reaching defs
*
* it uses obsolete dfa analysis under the hood for pretty unclear reasons
*/
@Nullable
ScopeVariable getDeclaredVariable(@NotNull PsiElement anchorElement, @NotNull String name) throws DFALimitExceededException;
@Deprecated
@ApiStatus.Internal
ScopeVariable getDeclaredVariable(@NotNull PsiElement anchorElement,
@NotNull String name,
@NotNull TypeEvalContext typeEvalContext) throws DFALimitExceededException;
boolean hasGlobals();
boolean isGlobal(String name);

View File

@@ -18,6 +18,7 @@ import com.jetbrains.python.codeInsight.dataflow.scope.ScopeVariable;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyAugAssignmentStatementNavigator;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -47,8 +48,9 @@ public class ScopeImpl implements Scope {
@Override
public ScopeVariable getDeclaredVariable(@NotNull final PsiElement anchorElement,
@NotNull final String name) throws DFALimitExceededException {
computeScopeVariables();
@NotNull final String name,
@NotNull final TypeEvalContext typeEvalContext) throws DFALimitExceededException {
computeScopeVariables(typeEvalContext);
for (int i = 0; i < myFlow.length; i++) {
Instruction instruction = myFlow[i];
final PsiElement element = instruction.getElement();
@@ -59,10 +61,10 @@ public class ScopeImpl implements Scope {
return null;
}
private synchronized void computeScopeVariables() throws DFALimitExceededException {
private synchronized void computeScopeVariables(@NotNull TypeEvalContext typeEvalContext) throws DFALimitExceededException {
computeFlow();
if (myCachedScopeVariables == null) {
final PyReachingDefsDfaInstance dfaInstance = new PyReachingDefsDfaInstance();
final PyReachingDefsDfaInstance dfaInstance = new PyReachingDefsDfaInstance(typeEvalContext);
final PyReachingDefsSemilattice semilattice = new PyReachingDefsSemilattice();
final DFAMapEngine<ScopeVariable> engine = new DFAMapEngine<>(myFlow, dfaInstance, semilattice);
myCachedScopeVariables = engine.performDFA();

View File

@@ -6,13 +6,17 @@ 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.jetbrains.python.codeInsight.controlflow.CallInstruction;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
public final class PyInspectionsUtil {
public static boolean hasAnyInterruptedControlFlowPaths(@NotNull PsiElement element) {
@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);
@@ -21,7 +25,7 @@ public final class PyInspectionsUtil {
if (start >= 0) {
final Ref<Boolean> resultRef = Ref.create(false);
ControlFlowUtil.iteratePrev(start, instructions, instruction -> {
if (instruction.allPred().isEmpty() && !isFirstInstruction(instruction)) {
if (CallInstruction.Companion.allPredWithoutNoReturn(instruction, context).isEmpty() && !isFirstInstruction(instruction)) {
resultRef.set(true);
return ControlFlowUtil.Operation.BREAK;
}
@@ -33,6 +37,7 @@ public final class PyInspectionsUtil {
return false;
}
@ApiStatus.Internal
static boolean isFirstInstruction(Instruction instruction) {
return instruction.num() == 0;
}

View File

@@ -98,7 +98,7 @@ public final class PyUnboundLocalVariableInspection extends PyInspection {
}
final ScopeVariable variable;
try {
variable = scope.getDeclaredVariable(anchor, name);
variable = scope.getDeclaredVariable(anchor, name, myTypeEvalContext);
}
catch (DFALimitExceededException e) {
largeFunctions.add(owner);
@@ -123,7 +123,7 @@ public final class PyUnboundLocalVariableInspection extends PyInspection {
if (resolvedUnderWithStatement(node, resolved) || resolvedUnderAssignmentExpressionAndCondition(node, resolved)) {
return;
}
if (PyInspectionsUtil.hasAnyInterruptedControlFlowPaths(node)) {
if (PyInspectionsUtil.hasAnyInterruptedControlFlowPaths(node, myTypeEvalContext)) {
return;
}
if (owner instanceof PyFile) {
@@ -167,7 +167,7 @@ public final class PyUnboundLocalVariableInspection extends PyInspection {
) != null;
}
private static boolean isFirstUnboundRead(@NotNull PyReferenceExpression node, @NotNull ScopeOwner owner) {
private boolean isFirstUnboundRead(@NotNull PyReferenceExpression node, @NotNull ScopeOwner owner) {
final String nodeName = node.getReferencedName();
final Scope scope = ControlFlowCache.getScope(owner);
final ControlFlow flow = ControlFlowCache.getControlFlow(owner);
@@ -183,7 +183,7 @@ public final class PyUnboundLocalVariableInspection extends PyInspection {
final PsiElement element = rwInstruction.getElement();
if (element != null && name != null && name.equals(nodeName) && instruction.num() < num) {
try {
if (scope.getDeclaredVariable(element, name) == null) {
if (scope.getDeclaredVariable(element, name, myTypeEvalContext) == null) {
final ReadWriteInstruction.ACCESS access = rwInstruction.getAccess();
if (access.isReadAccess()) {
first.set(false);

View File

@@ -295,7 +295,7 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
return;
}
if (!expr.isQualified()) {
if (PyInspectionsUtil.hasAnyInterruptedControlFlowPaths(expr)) {
if (PyInspectionsUtil.hasAnyInterruptedControlFlowPaths(expr, myTypeEvalContext)) {
return;
}
ContainerUtil.addIfNotNull(fixes, getTrueFalseQuickFix(refText));