From 17d5a4bbcedf3a45982d7a556d7b6d0b01690c87 Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Tue, 14 Apr 2020 01:06:46 +0300 Subject: [PATCH] Restrict matching against callables (PY-29993, PY-28364) Expected class-like types are not considered as callables (exclusion: bare typing.Callable). Fix matching definition against metaclass. GitOrigin-RevId: a2fe59c3bc7f56ef08dfb960a9ed841a86a65815 --- .../python/psi/types/PyTypeChecker.java | 9 ++++- .../ClassObjectAndMetaclassCompatibility.py | 15 ++++++++ .../PyTypeCheckerInspectionTest.java | 35 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java index 4d54b930b61a..b0f77c672d16 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -11,6 +11,7 @@ import com.jetbrains.python.PyNames; import com.jetbrains.python.PythonRuntimeService; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.codeInsight.typing.PyProtocolsKt; +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyTypeProvider; @@ -450,6 +451,12 @@ public class PyTypeChecker { private static Optional match(@NotNull PyCallableType expected, @NotNull PyCallableType actual, @NotNull MatchContext matchContext) { + if (expected instanceof PyClassLikeType) { + return PyTypingTypeProvider.CALLABLE.equals(((PyClassLikeType)expected).getClassQName()) + ? Optional.of(actual.isCallable()) + : Optional.empty(); + } + if (expected.isCallable() && actual.isCallable()) { final TypeEvalContext context = matchContext.context; final List expectedParameters = expected.getParameters(context); @@ -489,7 +496,7 @@ public class PyTypeChecker { return false; } final PyClassLikeType metaClass = actual.getMetaClassType(context.context, true); - return metaClass != null && match(expected, metaClass, context).orElse(true); + return metaClass != null && match(expected, metaClass.toInstance(), context).orElse(true); } private static boolean typeVarAcceptsBothClassAndInstanceTypes(@NotNull PyGenericType typeVar) { diff --git a/python/testData/inspections/PyTypeCheckerInspection/ClassObjectAndMetaclassCompatibility.py b/python/testData/inspections/PyTypeCheckerInspection/ClassObjectAndMetaclassCompatibility.py index 5a8c75ab422d..403079b5030d 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/ClassObjectAndMetaclassCompatibility.py +++ b/python/testData/inspections/PyTypeCheckerInspection/ClassObjectAndMetaclassCompatibility.py @@ -6,6 +6,10 @@ class SubMetaClass(MetaClass): pass +class MetaClass2(type): + pass + + class MyClass(object): __metaclass__ = MetaClass pass @@ -26,6 +30,15 @@ class MyClass3(Generated): pass +class MyClass4: + __metaclass__ = MetaClass2 + pass + + +class MyClass5: + pass + + def f(x): # type: (MetaClass) -> None pass @@ -35,3 +48,5 @@ f(MyClass) f(MyClass2) f(Generated) f(MyClass3) +f(MyClass4) +f(MyClass5) diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index 01c678cb9af2..b50fd0bf018a 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -1083,4 +1083,39 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase { public void testTypedDictInStub() { runWithLanguageLevel(LanguageLevel.getLatest(), this::doMultiFileTest); } + + // PY-28364 + public void testDefinitionAgainstCallableInstance() { + runWithLanguageLevel( + LanguageLevel.getLatest(), + () -> doTestByText("class B:\n" + + " def __call__(self, *args, **kwargs):\n" + + " pass\n" + + "\n" + + "def some_fn(arg: B):\n" + + " pass\n" + + "\n" + + "some_fn(B)") + ); + } + + // PY-29993 + public void testCallableInstanceAgainstOtherCallableInstance() { + runWithLanguageLevel( + LanguageLevel.getLatest(), + () -> doTestByText("class MyCls:\n" + + " def __call__(self):\n" + + " return True\n" + + "\n" + + "class DifferentCls:\n" + + " def __call__(self):\n" + + " return True\n" + + "\n" + + "def foo(arg: MyCls):\n" + + " pass\n" + + "\n" + + "foo(MyCls())\n" + + "foo(DifferentCls())") + ); + } }