From 4e69ba030f56ff0dffb65634cb8d864d448f1e24 Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Wed, 23 May 2018 16:55:23 +0300 Subject: [PATCH] Check parameters passed to Callable and suggest quick fixes (PY-20530) --- .../inspections/PyTypeHintsInspection.kt | 99 +++++++++++++++++-- .../callableMoreThanTwoElements.py | 3 + .../callableMoreThanTwoElements_after.py | 3 + .../callableTwoElements.py | 3 + ...callableTwoElementsParenthesizedAsFirst.py | 3 + ...leTwoElementsParenthesizedAsFirst_after.py | 3 + .../callableTwoElementsTupleAsFirst.py | 3 + .../callableTwoElementsTupleAsFirst_after.py | 3 + .../callableTwoElements_after.py | 3 + .../PyTypeHintsInspectionTest.java | 19 ++++ .../quickFixes/PyTypeHintsQuickFixTest.kt | 20 ++++ 11 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements.py create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements_after.py create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements.py create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst.py create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst_after.py create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst.py create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst_after.py create mode 100644 python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements_after.py diff --git a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index 513faba92009..c9bbe214e623 100644 --- a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -4,6 +4,7 @@ package com.jetbrains.python.inspections import com.intellij.codeInsight.controlflow.ControlFlowUtil import com.intellij.codeInspection.* import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFileFactory @@ -64,15 +65,18 @@ class PyTypeHintsInspection : PyInspection() { } } - override fun visitPySubscriptionExpression(node: PySubscriptionExpression?) { + override fun visitPySubscriptionExpression(node: PySubscriptionExpression) { super.visitPySubscriptionExpression(node) - if (node != null) { - val callee = node.operand as? PyReferenceExpression - val calleeQName = callee?.let { PyResolveUtil.resolveImportedElementQNameLocally(it) } ?: emptyList() + val operand = node.operand as? PyReferenceExpression ?: return + val index = node.indexExpression ?: return - if (genericQName in calleeQName) { - checkGenericParameters(node.indexExpression) + val callableQName = QualifiedName.fromDottedString(PyTypingTypeProvider.CALLABLE) + + PyResolveUtil.resolveImportedElementQNameLocally(operand).forEach { + when (it) { + genericQName -> checkGenericParameters(index) + callableQName -> checkCallableParameters(index) } } } @@ -386,7 +390,7 @@ class PyTypeHintsInspection : PyInspection() { return Pair(if (seenGeneric) genericTypeVars else null, nonGenericTypeVars) } - private fun checkGenericParameters(index: PyExpression?) { + private fun checkGenericParameters(index: PyExpression) { val parameters = (index as? PyTupleExpression)?.elements ?: arrayOf(index) val typeVars = mutableSetOf() @@ -411,6 +415,41 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkCallableParameters(index: PyExpression) { + val message = "'Callable' must be used as 'Callable[[arg, ...], result]'" + + if (index !is PyTupleExpression) { + registerProblem(index, message, ProblemHighlightType.GENERIC_ERROR) + return + } + + val parameters = index.elements + if (parameters.size > 2) { + val possiblyLastParameter = parameters[parameters.size - 2] + + registerProblem(index, + message, + ProblemHighlightType.GENERIC_ERROR, + null, + TextRange.create(0, possiblyLastParameter.startOffsetInParent + possiblyLastParameter.textLength), + SurroundElementsWithSquareBracketsQuickFix()) + } + else if (parameters.size < 2) { + registerProblem(index, message, ProblemHighlightType.GENERIC_ERROR) + } + else { + val first = parameters.first() + + if (first !is PyListLiteralExpression && !(first is PyNoneLiteralExpression && first.isEllipsis)) { + registerProblem(first, + message, + ProblemHighlightType.GENERIC_ERROR, + null, + if (first is PyParenthesizedExpression) ReplaceWithListQuickFix() else SurroundElementWithSquareBracketsQuickFix()) + } + } + } + private fun followNotTypingOpaque(target: PyTargetExpression): Boolean { return !PyTypingTypeProvider.OPAQUE_NAMES.contains(target.qualifiedName) } @@ -472,5 +511,51 @@ class PyTypeHintsInspection : PyInspection() { ?.let { element.replace(it) } } } + + private class SurroundElementsWithSquareBracketsQuickFix : LocalQuickFix { + + override fun getFamilyName() = "Surround with square brackets" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement as? PyTupleExpression ?: return + val list = PyElementGenerator.getInstance(project).createListLiteral() + + val originalElements = element.elements + originalElements.dropLast(1).forEach { list.add(it) } + originalElements.dropLast(2).forEach { it.delete() } + + element.elements.first().replace(list) + } + } + + private class SurroundElementWithSquareBracketsQuickFix : LocalQuickFix { + + override fun getFamilyName() = "Surround with square brackets" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement + val list = PyElementGenerator.getInstance(project).createListLiteral() + + list.add(element) + + element.replace(list) + } + } + + private class ReplaceWithListQuickFix : LocalQuickFix { + + override fun getFamilyName() = "Replace with square brackets" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement + + val expression = (element as? PyParenthesizedExpression)?.containedExpression ?: return + val elements = expression.let { if (it is PyTupleExpression) it.elements else arrayOf(it) } + + val list = PyElementGenerator.getInstance(project).createListLiteral() + elements.forEach { list.add(it) } + element.replace(list) + } + } } } \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements.py new file mode 100644 index 000000000000..8804775eb67d --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements.py @@ -0,0 +1,3 @@ +from typing import Callable + +f: Callable[int, str, str] \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements_after.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements_after.py new file mode 100644 index 000000000000..59d2e89e01e1 --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableMoreThanTwoElements_after.py @@ -0,0 +1,3 @@ +from typing import Callable + +f: Callable[[int, str], str] \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements.py new file mode 100644 index 000000000000..1d147a93f59d --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements.py @@ -0,0 +1,3 @@ +from typing import Callable + +e: Callable[int, str] \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst.py new file mode 100644 index 000000000000..9099038a402d --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst.py @@ -0,0 +1,3 @@ +from typing import Callable + +g: Callable[(int), str] \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst_after.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst_after.py new file mode 100644 index 000000000000..689adae701bb --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsParenthesizedAsFirst_after.py @@ -0,0 +1,3 @@ +from typing import Callable + +g: Callable[[int], str] \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst.py new file mode 100644 index 000000000000..79beac017a6c --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst.py @@ -0,0 +1,3 @@ +from typing import Callable + +g: Callable[(int, str), str] \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst_after.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst_after.py new file mode 100644 index 000000000000..f9f06706d71b --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElementsTupleAsFirst_after.py @@ -0,0 +1,3 @@ +from typing import Callable + +g: Callable[[int, str], str] \ No newline at end of file diff --git a/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements_after.py b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements_after.py new file mode 100644 index 000000000000..c19c6a9ec105 --- /dev/null +++ b/python/testData/quickFixes/PyTypeHintsQuickFixTest/callableTwoElements_after.py @@ -0,0 +1,3 @@ +from typing import Callable + +e: Callable[[int], str] \ 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 040a9ad28257..0a036bfbbc37 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -534,6 +534,25 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { ); } + // PY-20530 + public void testCallableParameters() { + runWithLanguageLevel( + LanguageLevel.PYTHON36, + () -> doTestByText("from typing import Callable\n" + + "\n" + + "a: Callable[..., str]\n" + + "b: Callable[[int], str]\n" + + "c: Callable[[int, str], str]\n" + + "\n" + + "d: Callable[...]\n" + + "e: Callable[int, str]\n" + + "f: Callable[int, str, str]\n" + + "g: Callable[(int, str), str]\n" + + "h: Callable[int]\n" + + "h: Callable[(int), str]") + ); + } + @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 c67e10c3046b..f9f302c5077c 100644 --- a/python/testSrc/com/jetbrains/python/quickFixes/PyTypeHintsQuickFixTest.kt +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyTypeHintsQuickFixTest.kt @@ -65,4 +65,24 @@ class PyTypeHintsQuickFixTest : PyQuickFixTestCase() { fun testInstanceCheckOnOperandReference() { doQuickFixTest(PyTypeHintsInspection::class.java, "Remove generic parameter(s)") } + + // PY-20530 + fun testCallableMoreThanTwoElements() { + doQuickFixTest(PyTypeHintsInspection::class.java, "Surround with square brackets", LanguageLevel.PYTHON37) + } + + // PY-20530 + fun testCallableTwoElements() { + doQuickFixTest(PyTypeHintsInspection::class.java, "Surround with square brackets", LanguageLevel.PYTHON37) + } + + // PY-20530 + fun testCallableTwoElementsTupleAsFirst() { + doQuickFixTest(PyTypeHintsInspection::class.java, "Replace with square brackets", LanguageLevel.PYTHON37) + } + + // PY-20530 + fun testCallableTwoElementsParenthesizedAsFirst() { + doQuickFixTest(PyTypeHintsInspection::class.java, "Replace with square brackets", LanguageLevel.PYTHON37) + } }