diff --git a/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt b/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt index ac12a7555503..493c03a4e57c 100644 --- a/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt @@ -5,8 +5,10 @@ import com.intellij.codeInspection.LocalInspectionToolSession import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.intellij.psi.impl.source.resolve.FileContextUtil +import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyParameterTypeList import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.getFunctionTypeAnnotation +import com.jetbrains.python.documentation.doctest.PyDocstringFile import com.jetbrains.python.psi.* import com.jetbrains.python.psi.PyKnownDecoratorUtil.KnownDecorator.TYPING_FINAL import com.jetbrains.python.psi.PyKnownDecoratorUtil.KnownDecorator.TYPING_FINAL_EXT @@ -93,6 +95,23 @@ class PyFinalInspection : PyInspection() { } } + override fun visitPyReferenceExpression(node: PyReferenceExpression) { + super.visitPyReferenceExpression(node) + + checkFinalIsOuterMost(node) + } + + private fun checkFinalIsOuterMost(node: PyReferenceExpression) { + if (isTopLevelInAnnotationOrTypeComment(node)) return + (node.parent as? PySubscriptionExpression)?.let { + if (it.operand == node && isTopLevelInAnnotationOrTypeComment(it)) return + } + + if (PyTypingTypeProvider.isInAnnotationOrTypeComment(node) && resolvesToFinal(node)) { + registerProblem(node, "'Final' could only be used as the outermost type") + } + } + private fun isFinal(decoratable: PyDecoratable): Boolean { return PyKnownDecoratorUtil.getKnownDecorators(decoratable, myTypeEvalContext).any { it == TYPING_FINAL || it == TYPING_FINAL_EXT } } @@ -120,5 +139,12 @@ class PyFinalInspection : PyInspection() { val file = FileContextUtil.getContextFile(node) ?: return null return PyUtil.createExpressionFromFragment(typeComment, file) } + + private fun isTopLevelInAnnotationOrTypeComment(node: PyExpression): Boolean { + if (node.parent is PyAnnotation) return true + if (node.parent is PyExpressionStatement && node.parent.parent is PyDocstringFile) return true + if (node.parent is PyParameterTypeList) return true + return false + } } } \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java index 26e125249fc0..347e959f6cca 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java @@ -142,6 +142,26 @@ public class PyFinalInspectionTest extends PyInspectionTestCase { ); } + // PY-34945 + public void testOuterMostFinal() { + runWithLanguageLevel( + LanguageLevel.PYTHON36, + () -> doTestByText("from typing_extensions import Final\n" + + "\n" + + "a1: Final[int] = 10\n" + + "b1: List[Final[int]] = []\n" + + "\n" + + "a2 = 10 # type: Final[int]\n" + + "b2 = [] # type: List[Final[int]]\n" + + "\n" + + "a3: Final = 10\n" + + "b3: List[Final] = []\n" + + "\n" + + "a4 = 10 # type: Final\n" + + "b4 = [] # type: List[Final]") + ); + } + @NotNull @Override protected Class getInspectionClass() {