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
This commit is contained in:
Mikhail Golubev
2022-07-19 15:06:06 +00:00
committed by intellij-monorepo-bot
parent 847cffdc6b
commit 63461fb262
17 changed files with 117 additions and 11 deletions
@@ -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,
@@ -0,0 +1,5 @@
class C:
def method(self, a: int, b: int) -> int: ...
_inst: C = ...
func = _inst.method
@@ -0,0 +1,5 @@
class C:
def method(self, a: int, b: int) -> int: ...
_inst: C = ...
func = _inst.method
@@ -0,0 +1,6 @@
class C:
@staticmethod
def method(a: int, b: int) -> int: ...
_inst: C = ...
func = _inst.method
@@ -0,0 +1,3 @@
from mod import func
func(1, 2, <warning descr="Unexpected argument"><caret>3</warning>)
@@ -0,0 +1,6 @@
class C:
def method(self, a, b):
pass
_inst: C = C()
func = _inst.method
@@ -0,0 +1,6 @@
class C:
def method(self, a, b, i):
pass
_inst: C = C()
func = _inst.method
@@ -0,0 +1,4 @@
import mod
mod.func(<arg>42)
@@ -0,0 +1,7 @@
class C:
def method(self, param):
pass
_inst: C = C()
func = _inst.method
@@ -0,0 +1,4 @@
import mod
mod.func(<arg>42)
@@ -0,0 +1,6 @@
class C:
def method(self, param):
pass
func = C.method
@@ -311,6 +311,18 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("self: A, a, b", new String[]{"b"}, new String[]{"self: A, "});
}
// PY-53671
public void testUnboundMethodReassignedAndImportedWithQualifiedImport() {
Map<String, PsiElement> marks = loadMultiFileTest(1);
feignCtrlP(marks.get("<arg>").getTextOffset()).check("self: C, param", new String[]{"self: C, "}, ArrayUtil.EMPTY_STRING_ARRAY);
}
// PY-53671
public void testBoundMethodReassignedAndImportedWithQualifiedImport() {
Map<String, PsiElement> marks = loadMultiFileTest(1);
feignCtrlP(marks.get("<arg>").getTextOffset()).check("self: C, param", new String[]{"param"}, new String[]{"self: C, "});
}
public void testConstructorFactory() {
Map<String, PsiElement> marks = loadTest(1);
@@ -747,6 +747,13 @@ public class PyQuickFixTest extends PyTestCase {
doInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of func(i1, <b>i</b>, <b>i3</b>, <b>num</b>)</html>", true, true);
}
// PY-53671
public void testChangeSignatureOfExportedBoundMethod() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
doMultiFilesInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of method(self, a, b, <b>i</b>)</html>", "mod.py");
});
}
// PY-8174
public void testChangeSignatureParametersDefaultValues() {
doInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of func(<b>i</b>, <b>foo</b>)</html>", true, true);
@@ -52,4 +52,26 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase {
"a = A(<warning descr=\"Parameter 'x' unfilled\">)</warning>\n" +
"b = B(<warning descr=\"Parameter 'x' unfilled\">)</warning>");
}
// 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)");
}
}