Highlight mismatching between type comment and parameters (PY-20530)

This commit is contained in:
Semyon Proshev
2018-06-13 23:12:48 +03:00
parent 746a89b076
commit 2bc9fc3ef2
3 changed files with 204 additions and 1 deletions
@@ -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;
@@ -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)
}
@@ -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" +
" <warning descr=\"Type signature has too many arguments\"># type: (Bar, int) -> None</warning>\n" +
" pass\n" +
"\n" +
" def egg11(self, a, b):\n" +
" <warning descr=\"Type signature has too many arguments\"># type: (Bar, int, str, bool) -> None</warning>\n" +
" pass\n" +
"\n" +
" def egg12(self, a, b):\n" +
" <warning descr=\"Type signature has too few arguments\"># type: (Bar) -> None</warning>\n" +
" pass\n" +
" \n" +
" # self is not specified\n" +
" def spam2(self):\n" +
" <warning descr=\"The type of self 'int' is not a supertype of its class 'Bar'\"># type: (int) -> None</warning>\n" +
" pass\n" +
"\n" +
" def egg2(self, a, b):\n" +
" <warning descr=\"The type of self 'int' is not a supertype of its class 'Bar'\"># type: (int, str, bool) -> None</warning>\n" +
" pass\n" +
" \n" +
" # cls is not specified \n" +
" @classmethod\n" +
" def spam3(cls):\n" +
" <warning descr=\"The type of self 'int' is not a supertype of its class 'Type[Bar]'\"># type: (int) -> None</warning>\n" +
" pass\n" +
"\n" +
" @classmethod\n" +
" def egg3(cls, a, b):\n" +
" <warning descr=\"The type of self 'int' is not a supertype of its class 'Type[Bar]'\"># type: (int, str, bool) -> None</warning>\n" +
" pass\n" +
" \n" +
" # cls is specified \n" +
" @classmethod\n" +
" def spam4(cls):\n" +
" <warning descr=\"Type signature has too many arguments\"># type: (Type[Bar], int) -> None</warning>\n" +
" pass\n" +
"\n" +
" @classmethod\n" +
" def egg41(cls, a, b):\n" +
" <warning descr=\"Type signature has too many arguments\"># type: (Type[Bar], int, str, bool) -> None</warning>\n" +
" pass\n" +
"\n" +
" @classmethod\n" +
" def egg42(cls, a, b):\n" +
" <warning descr=\"Type signature has too few arguments\"># type: (Type[Bar]) -> None</warning>\n" +
" pass\n" +
" \n" +
" @staticmethod\n" +
" def spam5():\n" +
" <warning descr=\"Type signature has too many arguments\"># type: (int) -> None</warning>\n" +
" pass\n" +
"\n" +
" @staticmethod\n" +
" def egg51(a, b):\n" +
" <warning descr=\"Type signature has too many arguments\"># type: (int, str, bool) -> None</warning>\n" +
" pass\n" +
"\n" +
" @staticmethod\n" +
" def egg52(a, b):\n" +
" <warning descr=\"Type signature has too few arguments\"># type: (int) -> None</warning>\n" +
" pass");
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {