From 63461fb262d741b23c86dc49e734c07f775f44b3 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Tue, 19 Jul 2022 12:09:59 +0300 Subject: [PATCH] PY-53671 Don't treat methods qualified with a module as unbound While resolving an aliased method exported from a module, we might lose the context that it was referenced via instance and hence its first "self" parameter is already bound and doesn't need to be passed explicitly. The reason is PyResolveUtil#doResolveQualifiedNameInScope (called in PyTargetExpressionImpl.multiResolveAssignedValue) performs resolve over qualified names saved in PSI stubs and returns plain PsiElements (end results) that don't retain such information about their qualifiers. QualifiedResolveResult can't be used there either because we don't keep PyExpressions in PSI stubs. What's more, when later such function is referenced via some module we consider it definitely unbound, even though a module cannot possibly have a method as its immediate attribute. I changed the logic so that we no longer consider referencing a method through a module as somehow affecting its bound/unbound state. GitOrigin-RevId: 17a6c3e5d43c088d0663ba54651004c8370d5eca --- .../psi/impl/PyCallExpressionHelper.java | 26 +++++++++++-------- .../a.py | 3 +++ .../mod.pyi | 5 ++++ .../a.py | 3 +++ .../mod.pyi | 5 ++++ .../a.py | 3 +++ .../mod.pyi | 6 +++++ .../changeSignatureOfExportedBoundMethod.py | 3 +++ .../mod.py | 6 +++++ .../mod_after.py | 6 +++++ .../a.py | 4 +++ .../mod.py | 7 +++++ .../a.py | 4 +++ .../mod.py | 6 +++++ .../jetbrains/python/PyParameterInfoTest.java | 12 +++++++++ .../com/jetbrains/python/PyQuickFixTest.java | 7 +++++ .../Py3ArgumentListInspectionTest.java | 22 ++++++++++++++++ 17 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/a.py create mode 100644 python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/mod.pyi create mode 100644 python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py create mode 100644 python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi create mode 100644 python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py create mode 100644 python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi create mode 100644 python/testData/inspections/changeSignatureOfExportedBoundMethod/changeSignatureOfExportedBoundMethod.py create mode 100644 python/testData/inspections/changeSignatureOfExportedBoundMethod/mod.py create mode 100644 python/testData/inspections/changeSignatureOfExportedBoundMethod/mod_after.py create mode 100644 python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/a.py create mode 100644 python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/mod.py create mode 100644 python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/a.py create mode 100644 python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/mod.py diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index 8d59d75df9fa..50201cdceffa 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -10,6 +10,7 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.ResolveResult; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ObjectUtils; +import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import com.jetbrains.python.PyNames; @@ -486,25 +487,28 @@ public final class PyCallExpressionHelper { return true; // unqualified + method = implicit constructor call } for (PyExpression qualifier : qualifiers) { - if (qualifier != null && isQualifiedByInstance(resolved, qualifier, context)) { - return true; + if (qualifier != null) { + ThreeState byInstance = isQualifiedByInstance(resolved, qualifier, context); + if (byInstance != ThreeState.UNSURE) { + return byInstance.toBoolean(); + } } } - return false; + return true; } - private static boolean isQualifiedByInstance(@Nullable PyCallable resolved, - @NotNull PyExpression qualifier, - @NotNull TypeEvalContext context) { + private static @NotNull ThreeState isQualifiedByInstance(@Nullable PyCallable resolved, + @NotNull PyExpression qualifier, + @NotNull TypeEvalContext context) { if (isQualifiedByClass(resolved, qualifier, context)) { - return false; + return ThreeState.NO; } final PyType qualifierType = context.getType(qualifier); - if (qualifierType != null) { - // TODO: handle UnionType - if (qualifierType instanceof PyModuleType) return false; // qualified by module, not instance. + // TODO: handle UnionType + if (qualifierType instanceof PyModuleType) { + return ThreeState.UNSURE; } - return true; // NOTE. best guess: unknown qualifier is more probably an instance. + return ThreeState.YES; // NOTE. best guess: unknown qualifier is more probably an instance. } private static boolean isQualifiedByClass(@Nullable PyCallable resolved, diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/a.py b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/a.py new file mode 100644 index 000000000000..9c5fa2502320 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/a.py @@ -0,0 +1,3 @@ +from mod import func + +func(1, 2) diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/mod.pyi b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/mod.pyi new file mode 100644 index 000000000000..d91d087fb4f2 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/mod.pyi @@ -0,0 +1,5 @@ +class C: + def method(self, a: int, b: int) -> int: ... + +_inst: C = ... +func = _inst.method \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..c66aa063d76c --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py @@ -0,0 +1,3 @@ +import mod + +mod.func(1, 2) \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi new file mode 100644 index 000000000000..d91d087fb4f2 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi @@ -0,0 +1,5 @@ +class C: + def method(self, a: int, b: int) -> int: ... + +_inst: C = ... +func = _inst.method \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..c66aa063d76c --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py @@ -0,0 +1,3 @@ +import mod + +mod.func(1, 2) \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi new file mode 100644 index 000000000000..690e7e972bc7 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi @@ -0,0 +1,6 @@ +class C: + @staticmethod + def method(a: int, b: int) -> int: ... + +_inst: C = ... +func = _inst.method diff --git a/python/testData/inspections/changeSignatureOfExportedBoundMethod/changeSignatureOfExportedBoundMethod.py b/python/testData/inspections/changeSignatureOfExportedBoundMethod/changeSignatureOfExportedBoundMethod.py new file mode 100644 index 000000000000..4757445c5264 --- /dev/null +++ b/python/testData/inspections/changeSignatureOfExportedBoundMethod/changeSignatureOfExportedBoundMethod.py @@ -0,0 +1,3 @@ +from mod import func + +func(1, 2, 3) \ No newline at end of file diff --git a/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod.py b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod.py new file mode 100644 index 000000000000..a3b7d022b6f6 --- /dev/null +++ b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod.py @@ -0,0 +1,6 @@ +class C: + def method(self, a, b): + pass + +_inst: C = C() +func = _inst.method \ No newline at end of file diff --git a/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod_after.py b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod_after.py new file mode 100644 index 000000000000..0e9bd16c1026 --- /dev/null +++ b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod_after.py @@ -0,0 +1,6 @@ +class C: + def method(self, a, b, i): + pass + +_inst: C = C() +func = _inst.method \ No newline at end of file diff --git a/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/a.py b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..ff45bddfdb33 --- /dev/null +++ b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/a.py @@ -0,0 +1,4 @@ +import mod + + +mod.func(42) \ No newline at end of file diff --git a/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/mod.py b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/mod.py new file mode 100644 index 000000000000..4df3d92d0ae0 --- /dev/null +++ b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/mod.py @@ -0,0 +1,7 @@ +class C: + def method(self, param): + pass + + +_inst: C = C() +func = _inst.method diff --git a/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/a.py b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..ff45bddfdb33 --- /dev/null +++ b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/a.py @@ -0,0 +1,4 @@ +import mod + + +mod.func(42) \ No newline at end of file diff --git a/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/mod.py b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/mod.py new file mode 100644 index 000000000000..644f81777a92 --- /dev/null +++ b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/mod.py @@ -0,0 +1,6 @@ +class C: + def method(self, param): + pass + + +func = C.method diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index 559c8395e49b..149ce4a771a9 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -311,6 +311,18 @@ public class PyParameterInfoTest extends LightMarkedTestCase { feignCtrlP(marks.get("").getTextOffset()).check("self: A, a, b", new String[]{"b"}, new String[]{"self: A, "}); } + // PY-53671 + public void testUnboundMethodReassignedAndImportedWithQualifiedImport() { + Map marks = loadMultiFileTest(1); + feignCtrlP(marks.get("").getTextOffset()).check("self: C, param", new String[]{"self: C, "}, ArrayUtil.EMPTY_STRING_ARRAY); + } + + // PY-53671 + public void testBoundMethodReassignedAndImportedWithQualifiedImport() { + Map marks = loadMultiFileTest(1); + feignCtrlP(marks.get("").getTextOffset()).check("self: C, param", new String[]{"param"}, new String[]{"self: C, "}); + } + public void testConstructorFactory() { Map marks = loadTest(1); diff --git a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java index 97b9a05be626..9d57240451cc 100644 --- a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java +++ b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java @@ -747,6 +747,13 @@ public class PyQuickFixTest extends PyTestCase { doInspectionTest(PyArgumentListInspection.class, "Change the signature of func(i1, i, i3, num)", true, true); } + // PY-53671 + public void testChangeSignatureOfExportedBoundMethod() { + runWithLanguageLevel(LanguageLevel.getLatest(), () -> { + doMultiFilesInspectionTest(PyArgumentListInspection.class, "Change the signature of method(self, a, b, i)", "mod.py"); + }); + } + // PY-8174 public void testChangeSignatureParametersDefaultValues() { doInspectionTest(PyArgumentListInspection.class, "Change the signature of func(i, foo)", true, true); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java index 6c913772e4c3..68be755d346d 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java @@ -52,4 +52,26 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase { "a = A()\n" + "b = B()"); } + + // PY-53671 + public void testBoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport() { + doMultiFileTest(); + } + + // PY-53671 + public void testBoundMethodExportedAsTopLevelFunctionImportedWithFromImport() { + doMultiFileTest(); + } + + // PY-53671 + public void testStaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport() { + doMultiFileTest(); + } + + // PY-53671 + public void testRandomRandint() { + doTestByText("import random\n" + + "\n" + + "random.randint(1, 2)"); + } }