diff --git a/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java
index fe8f8b5d4446..e05b02f2234d 100644
--- a/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java
+++ b/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java
@@ -362,7 +362,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
}
@Nullable
- private static PyFunctionTypeAnnotation getFunctionTypeAnnotation(@NotNull PyFunction function) {
+ public static PyFunctionTypeAnnotation getFunctionTypeAnnotation(@NotNull PyFunction function) {
final String comment = function.getTypeCommentAnnotation();
if (comment == null) {
return null;
diff --git a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt
index d5eed817f235..fc83eeeb9159 100644
--- a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt
+++ b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt
@@ -15,6 +15,7 @@ import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.codeInsight.functionTypeComments.PyFunctionTypeAnnotationDialect
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
+import com.jetbrains.python.documentation.PythonDocumentationProvider
import com.jetbrains.python.documentation.doctest.PyDocstringFile
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyEvaluator
@@ -22,6 +23,7 @@ import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.resolve.PyResolveUtil
import com.jetbrains.python.psi.types.PyGenericType
+import com.jetbrains.python.psi.types.PyInstantiableType
import com.jetbrains.python.psi.types.PyTypeChecker
class PyTypeHintsInspection : PyInspection() {
@@ -121,6 +123,12 @@ class PyTypeHintsInspection : PyInspection() {
}
}
+ override fun visitPyFunction(node: PyFunction) {
+ super.visitPyFunction(node)
+
+ checkTypeCommentAndParameters(node)
+ }
+
private fun checkTypeVarPlacement(call: PyCallExpression, target: PyExpression?) {
if (target == null) {
registerProblem(call, "A 'TypeVar()' expression must always directly be assigned to a variable")
@@ -502,6 +510,48 @@ class PyTypeHintsInspection : PyInspection() {
}
}
+ private fun checkTypeCommentAndParameters(node: PyFunction) {
+ val functionTypeAnnotation = PyTypingTypeProvider.getFunctionTypeAnnotation(node) ?: return
+
+ val parameterTypes = functionTypeAnnotation.parameterTypeList.parameterTypes
+ if (parameterTypes.singleOrNull().let { it is PyNoneLiteralExpression && it.isEllipsis }) return
+
+ val actualParametersSize = node.parameterList.parameters.size
+ val commentParametersSize = parameterTypes.size
+
+ val cls = node.containingClass
+ val modifier = node.modifier
+
+ val hasSelf = cls != null && modifier != PyFunction.Modifier.STATICMETHOD
+
+ if (commentParametersSize < actualParametersSize - if (hasSelf) 1 else 0) {
+ registerProblem(node.typeComment, "Type signature has too few arguments")
+ }
+ else if (commentParametersSize > actualParametersSize) {
+ registerProblem(node.typeComment, "Type signature has too many arguments")
+ }
+ else if (hasSelf && actualParametersSize == commentParametersSize) {
+ val actualSelfType =
+ (myTypeEvalContext.getType(cls!!) as? PyInstantiableType<*>)
+ ?.let { if (modifier == PyFunction.Modifier.CLASSMETHOD) it.toClass() else it.toInstance() }
+ ?: return
+
+ val commentSelfType =
+ parameterTypes.firstOrNull()
+ ?.let { PyTypingTypeProvider.getType(it, myTypeEvalContext) }
+ ?.get()
+ ?: return
+
+ if (!PyTypeChecker.match(commentSelfType, actualSelfType, myTypeEvalContext)) {
+ val actualSelfTypeDescription = PythonDocumentationProvider.getTypeDescription(actualSelfType, myTypeEvalContext)
+ val commentSelfTypeDescription = PythonDocumentationProvider.getTypeDescription(commentSelfType, myTypeEvalContext)
+
+ registerProblem(node.typeComment,
+ "The type of self '$commentSelfTypeDescription' is not a supertype of its class '$actualSelfTypeDescription'")
+ }
+ }
+ }
+
private fun followNotTypingOpaque(target: PyTargetExpression): Boolean {
return !PyTypingTypeProvider.OPAQUE_NAMES.contains(target.qualifiedName)
}
diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java
index 2b121430458b..940ad90e04bb 100644
--- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java
+++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java
@@ -610,6 +610,159 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase {
);
}
+ // PY-20530
+ public void testValidTypeCommentAndParameters() {
+ doTestByText("from typing import Type\n" +
+ "\n" +
+ "class A:\n" +
+ " pass\n" +
+ "\n" +
+ "class Bar(A):\n" +
+ " # self is specified\n" +
+ " def spam11(self):\n" +
+ " # type: (Bar) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " def egg11(self, a, b):\n" +
+ " # type: (Bar, str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # self is specified\n" +
+ " def spam12(self):\n" +
+ " # type: (A) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " def egg12(self, a, b):\n" +
+ " # type: (A, str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # self is not specified\n" +
+ " def spam2(self):\n" +
+ " # type: () -> None\n" +
+ " pass\n" +
+ "\n" +
+ " def egg2(self, a, b):\n" +
+ " # type: (str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # cls is not specified \n" +
+ " @classmethod\n" +
+ " def spam3(cls):\n" +
+ " # type: () -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @classmethod\n" +
+ " def egg3(cls, a, b):\n" +
+ " # type: (str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # cls is specified \n" +
+ " @classmethod\n" +
+ " def spam41(cls):\n" +
+ " # type: (Type[Bar]) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @classmethod\n" +
+ " def egg41(cls, a, b):\n" +
+ " # type: (Type[Bar], str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # cls is specified \n" +
+ " @classmethod\n" +
+ " def spam42(cls):\n" +
+ " # type: (Type[A]) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @classmethod\n" +
+ " def egg42(cls, a, b):\n" +
+ " # type: (Type[A], str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " @staticmethod\n" +
+ " def spam5():\n" +
+ " # type: () -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @staticmethod\n" +
+ " def egg5(a, b):\n" +
+ " # type: (str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " def baz(self, a, b, c, d):\n" +
+ " # type: (...) -> None\n" +
+ " pass");
+ }
+
+ // PY-20530
+ public void testInvalidTypeCommentAndParameters() {
+ doTestByText("from typing import Type\n" +
+ "\n" +
+ "class Bar:\n" +
+ " # self is specified\n" +
+ " def spam1(self):\n" +
+ " # type: (Bar, int) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " def egg11(self, a, b):\n" +
+ " # type: (Bar, int, str, bool) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " def egg12(self, a, b):\n" +
+ " # type: (Bar) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # self is not specified\n" +
+ " def spam2(self):\n" +
+ " # type: (int) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " def egg2(self, a, b):\n" +
+ " # type: (int, str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # cls is not specified \n" +
+ " @classmethod\n" +
+ " def spam3(cls):\n" +
+ " # type: (int) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @classmethod\n" +
+ " def egg3(cls, a, b):\n" +
+ " # type: (int, str, bool) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " # cls is specified \n" +
+ " @classmethod\n" +
+ " def spam4(cls):\n" +
+ " # type: (Type[Bar], int) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @classmethod\n" +
+ " def egg41(cls, a, b):\n" +
+ " # type: (Type[Bar], int, str, bool) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @classmethod\n" +
+ " def egg42(cls, a, b):\n" +
+ " # type: (Type[Bar]) -> None\n" +
+ " pass\n" +
+ " \n" +
+ " @staticmethod\n" +
+ " def spam5():\n" +
+ " # type: (int) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @staticmethod\n" +
+ " def egg51(a, b):\n" +
+ " # type: (int, str, bool) -> None\n" +
+ " pass\n" +
+ "\n" +
+ " @staticmethod\n" +
+ " def egg52(a, b):\n" +
+ " # type: (int) -> None\n" +
+ " pass");
+ }
+
@NotNull
@Override
protected Class extends PyInspection> getInspectionClass() {