From b8a042e663352aea3e83c7ccd6c6fa59f4f433be Mon Sep 17 00:00:00 2001 From: Andrey Vokin Date: Mon, 15 Jun 2026 20:07:58 +0200 Subject: [PATCH] PY-89956 resolve local variable via PyDefUseUtil.getLatestDefs instead of PyResolveUtil.scopeCrawlUp When a name is defined in the reference's own scope, scopeCrawlUp collects all same-name definitions just to find the owner and then narrows to the reaching defs anyway; for a variable reassigned N times that makes resolving its N references O(N^2) and hangs inspection. Go straight to the control-flow reaching defs for the plain local case; everything else falls back to the general path unchanged. GitOrigin-RevId: 3694fdb0afd6077340e1d75eaf39090fe1f848a9 --- .../codeInsight/dataflow/scope/Scope.java | 3 +++ .../dataflow/scope/impl/ScopeImpl.java | 8 ++++++++ .../psi/impl/references/PyReferenceImpl.java | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/Scope.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/Scope.java index 8265559da00c..4705984b7b18 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/Scope.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/Scope.java @@ -52,6 +52,9 @@ public interface Scope { boolean containsDeclaration(String name); + /** Cheap O(1) check whether this scope itself declares a (non-import) named element {@code name}. */ + boolean declaresName(@NotNull String name); + @NotNull List getImportedNameDefiners(); diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/impl/ScopeImpl.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/impl/ScopeImpl.java index 09faeea488fa..8d4421d15303 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/impl/ScopeImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/dataflow/scope/impl/ScopeImpl.java @@ -169,6 +169,14 @@ public class ScopeImpl implements Scope { return false; } + @Override + public boolean declaresName(@NotNull String name) { + if (myNamedElements == null) { + collectDeclarations(); + } + return myNamedElements.containsKey(name); + } + @Override public @NotNull List getImportedNameDefiners() { if (myImportedNameDefiners == null) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java index 2c33c7ac5656..22d1ad948549 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java @@ -274,6 +274,24 @@ public class PyReferenceImpl implements PsiReferenceEx, PsiPolyVariantReference return ((PyFile)realContext).multiResolveName(referencedName); } + // PY-89956 fast path: for a plain local variable, resolve straight to its control-flow + // reaching definitions instead of first collecting *all* same-name definitions of the scope + final TypeEvalContext typeEvalContext = myContext.getTypeEvalContext(); + final ScopeOwner owner = ScopeUtil.getScopeOwner(realContext); + if (typeEvalContext.maySwitchToAST(realContext) && owner != null && !(owner instanceof PyClass)) { + final Scope scope = ControlFlowCache.getScope(owner); + if (scope.declaresName(referencedName) && !scope.isGlobal(referencedName) && !scope.isNonlocal(referencedName)) { + final List defs = + PyDefUseUtil.getLatestDefs(owner, referencedName, realContext, false, true, typeEvalContext).defs(); + if (!defs.isEmpty() && ContainerUtil.and(defs, i -> i.getElement() instanceof PyTargetExpression)) { + final ResolveResultList latest = resolveToLatestDefs(defs, realContext, referencedName, typeEvalContext); + if (!latest.isEmpty()) { + return latest; + } + } + } + } + // here we have an unqualified expr. it may be defined: // ...in current file final PyResolveProcessor processor = new PyResolveProcessor(referencedName);