From cb009592c3677bf8b92c94ff76c05d0ccb52f069 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Fri, 15 Nov 2024 22:24:43 +0200 Subject: [PATCH] PY-77168 Allow flow-sensitive resolve from unmatched version checks to reachable outer blocks Also, don't query LanguageLevel for each element of each instruction, only once per scope traversal. This doesn't fix the problem with unreachable definition *inside* blocks under unmatched version checks, i.e. if sys.version_info < (3, 8): Alias = int expr: Alias # unresolved but it's a more difficult problem how to handle those consistently with the idea of unreachable version checks under the *current* interpreter version, and hopefully it occurs rarer than, say, unresolved top-level imports of common names from typing. GitOrigin-RevId: 55fd4597c6d0860d290caba15fbf4d313e985a86 --- .../jetbrains/python/PyCommonResolveTest.java | 12 ++++++++++ .../python/refactoring/PyDefUseUtil.java | 17 +++++++------- .../com/jetbrains/python/PyTypingTest.java | 22 +++++++++++++++++++ ...Py3UnresolvedReferencesInspectionTest.java | 14 ++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java b/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java index 143c223a9861..2b9e7af91279 100644 --- a/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java +++ b/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java @@ -2133,6 +2133,18 @@ public abstract class PyCommonResolveTest extends PyCommonResolveTestCase { assertResolvedElement(LanguageLevel.PYTHON34, starImport, TestCase::assertNull); } + // PY-77168 + public void testResolveFromUnderUnmatchedVersionCheck() { + assertResolvesTo(""" + import sys + + Alias = int + if sys.version_info < (3, 12): + name: Alias + # + """, PyTargetExpression.class, "Alias"); + } + private void assertResolvedElement(@NotNull LanguageLevel languageLevel, @NotNull String text, @NotNull Consumer assertion) { runWithLanguageLevel(languageLevel, () -> { myFixture.configureByText(PythonFileType.INSTANCE, text); diff --git a/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java b/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java index c9e9836b639e..816b73ea287d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java @@ -86,9 +86,6 @@ public final class PyDefUseUtil { final HashMap pendingTypeGuard = new HashMap<>(); ControlFlowUtil.iteratePrev(startNum, instructions, instruction -> { - if (unreachableDueToVersionGuard(instruction)) { - return ControlFlowUtil.Operation.CONTINUE; - } if (acceptTypeAssertions && instruction instanceof CallInstruction callInstruction) { var typeGuardInstruction = pendingTypeGuard.get(instruction.getElement()); if (typeGuardInstruction != null) { @@ -123,14 +120,18 @@ public final class PyDefUseUtil { if (access.isWriteAccess() || acceptTypeAssertions && access.isAssertTypeAccess()) { final String name = elementName(element); if (Comparing.strEqual(name, varName)) { - result.add(rwInstruction); + if (isReachableWithVersionChecks(rwInstruction)) { + result.add(rwInstruction); + } return ControlFlowUtil.Operation.CONTINUE; } } } else if (acceptImplicitImports && element instanceof PyImplicitImportNameDefiner implicit) { if (!implicit.multiResolveName(varName).isEmpty()) { - result.add(instruction); + if (isReachableWithVersionChecks(instruction)) { + result.add(instruction); + } return ControlFlowUtil.Operation.CONTINUE; } } @@ -139,12 +140,12 @@ public final class PyDefUseUtil { return result; } - private static boolean unreachableDueToVersionGuard(@NotNull Instruction instruction) { + private static boolean isReachableWithVersionChecks(@NotNull Instruction instruction) { PsiElement element = instruction.getElement(); - if (element == null) return false; + if (element == null) return true; LanguageLevel languageLevel = PythonLanguageLevelPusher.getLanguageLevelForFile(element.getContainingFile()); Version version = new Version(languageLevel.getMajorVersion(), languageLevel.getMinorVersion(), 0); - return !evaluateVersionsForElement(element).contains(version); + return evaluateVersionsForElement(element).contains(version); } @Nullable diff --git a/python/testSrc/com/jetbrains/python/PyTypingTest.java b/python/testSrc/com/jetbrains/python/PyTypingTest.java index 8cbaf4a4a241..6fc30e68ffc6 100644 --- a/python/testSrc/com/jetbrains/python/PyTypingTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java @@ -5959,6 +5959,28 @@ public class PyTypingTest extends PyTestCase { """); } + // PY-77168 + public void testReferencingImportedTypeFromUnmatchedVersionGuard() { + doTest("Literal[42]", """ + from typing import Literal + import sys + + if sys.version_info < (3, 0): + expr: Literal[42] + """); + } + + // PY-77168 + public void testReferencingTopLevelTypeFromUnmatchedVersionGuard() { + doTest("int", """ + import sys + + type Alias = int + if sys.version_info < (3, 0): + expr: Alias + """); + } + // PY-76243 public void testGenericClassDeclaredInStubPackage() { runWithAdditionalClassEntryInSdkRoots("types/" + getTestName(false) + "/site-packages", () -> { diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java index fa78a5f4e9a6..a9047e540ca1 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java @@ -341,4 +341,18 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase pass """); } + + // PY-77168 + public void testReferenceFromUnderUnmatchedVersionCheck() { + doTestByText(""" + import sys + from typing import overload + + Alias = int + if sys.version_info < (3, 12): + @overload + def f() -> Alias: + ... + """); + } }