diff --git a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index aa8cf8d8d476..d5eed817f235 100644 --- a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -102,6 +102,25 @@ class PyTypeHintsInspection : PyInspection() { } } + override fun visitPyElement(node: PyElement?) { + super.visitPyElement(node) + + if (node is PyTypeCommentOwner && node is PyAnnotationOwner && node.typeCommentAnnotation != null) { + val message = "Type(s) specified both in type comment and annotation" + + if (node is PyFunction) { + if (node.annotationValue != null || node.parameterList.parameters.any { it is PyNamedParameter && it.annotationValue != null }) { + registerProblem(node.typeComment, message, RemoveElementQuickFix("Remove type comment")) + registerProblem(node.nameIdentifier, message, RemoveFunctionAnnotations()) + } + } + else if (node.annotationValue != null) { + registerProblem(node.typeComment, message, RemoveElementQuickFix("Remove type comment")) + registerProblem(node.annotation, message, RemoveElementQuickFix("Remove annotation")) + } + } + } + private fun checkTypeVarPlacement(call: PyCallExpression, target: PyExpression?) { if (target == null) { registerProblem(call, "A 'TypeVar()' expression must always directly be assigned to a variable") @@ -509,6 +528,29 @@ class PyTypeHintsInspection : PyInspection() { } } + private class RemoveElementQuickFix(private val description: String) : LocalQuickFix { + + override fun getFamilyName() = description + override fun applyFix(project: Project, descriptor: ProblemDescriptor) = descriptor.psiElement.delete() + } + + private class RemoveFunctionAnnotations : LocalQuickFix { + + override fun getFamilyName() = "Remove function annotations" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val function = (descriptor.psiElement.parent as? PyFunction) ?: return + + function.annotation?.delete() + + function.parameterList.parameters + .asSequence() + .filterIsInstance() + .mapNotNull { it.annotation } + .forEach { it.delete() } + } + } + private class ReplaceWithTargetNameQuickFix(private val targetName: String) : LocalQuickFix { override fun getFamilyName() = "Replace with target name" diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/functionAnnotationAndTypeComment.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/functionAnnotationAndTypeComment.py new file mode 100644 index 000000000000..0b1779963942 --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/functionAnnotationAndTypeComment.py @@ -0,0 +1,3 @@ +def bar(a: int) -> int: + # type: (int) -> int + pass \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/functionAnnotationAndTypeComment_after.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/functionAnnotationAndTypeComment_after.py new file mode 100644 index 000000000000..3b78f4884c70 --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/functionAnnotationAndTypeComment_after.py @@ -0,0 +1,3 @@ +def bar(a): + # type: (int) -> int + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index a1d29f8144ec..2b121430458b 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -584,6 +584,32 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { "e2, (f2, g2), h2 = undefined() # type: int, (str), str"); } + // PY-20530 + public void testAnnotationAndTypeComment() { + runWithLanguageLevel( + LanguageLevel.PYTHON36, + () -> doTestByText( + "a: int = None # type: int\n" + + "\n" + + "def foo(a: int # type: int\n" + + " ,):\n" + + " pass\n" + + "\n" + + "def bar(a: int) -> int:\n" + + " # type: (int) -> int\n" + + " pass\n" + + " \n" + + "def baz1(a: int):\n" + + " # type: (int) -> int\n" + + " pass\n" + + " \n" + + "def baz2(a) -> int:\n" + + " # type: (int) -> int\n" + + " pass" + ) + ); + } + @NotNull @Override protected Class getInspectionClass() { diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyTypeHintsQuickFixTest.kt b/python/testSrc/com/jetbrains/python/quickFixes/PyTypeHintsQuickFixTest.kt index 8efd5c0316c6..03269ca78b50 100644 --- a/python/testSrc/com/jetbrains/python/quickFixes/PyTypeHintsQuickFixTest.kt +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyTypeHintsQuickFixTest.kt @@ -95,4 +95,9 @@ class PyTypeHintsQuickFixTest : PyQuickFixTestCase() { fun testSelfInTypeComment() { doQuickFixTest(PyTypeHintsInspection::class.java, "Replace with type name") } + + // PY-20530 + fun testFunctionAnnotationAndTypeComment() { + doQuickFixTest(PyTypeHintsInspection::class.java, "Remove function annotations", LanguageLevel.PYTHON35) + } }