From 2bc9fc3ef24607013dca2615a334e216f36f0534 Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Mon, 28 May 2018 21:10:47 +0300 Subject: [PATCH] Highlight mismatching between type comment and parameters (PY-20530) --- .../typing/PyTypingTypeProvider.java | 2 +- .../inspections/PyTypeHintsInspection.kt | 50 ++++++ .../PyTypeHintsInspectionTest.java | 153 ++++++++++++++++++ 3 files changed, 204 insertions(+), 1 deletion(-) 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 getInspectionClass() {