diff --git a/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt b/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt index bbf2b02d53aa..ac12a7555503 100644 --- a/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt @@ -4,7 +4,9 @@ package com.jetbrains.python.inspections 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.typing.PyTypingTypeProvider +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.getFunctionTypeAnnotation import com.jetbrains.python.psi.* import com.jetbrains.python.psi.PyKnownDecoratorUtil.KnownDecorator.TYPING_FINAL import com.jetbrains.python.psi.PyKnownDecoratorUtil.KnownDecorator.TYPING_FINAL_EXT @@ -64,26 +66,33 @@ class PyFinalInspection : PyInspection() { else if (isFinal(node)) { registerProblem(node.nameIdentifier, "Non-method function could not be marked as '@final'") } + + getFunctionTypeAnnotation(node)?.let { comment -> + if (comment.parameterTypeList.parameterTypes.any { resolvesToFinal(if (it is PySubscriptionExpression) it.operand else it) }) { + registerProblem(node.typeComment, "'Final' could not be used in annotations for function parameters") + } + } } override fun visitPyTargetExpression(node: PyTargetExpression) { super.visitPyTargetExpression(node) if (!node.hasAssignedValue()) { - val value = node.annotation?.value - if (value is PyReferenceExpression) { - value - .multiFollowAssignmentsChain(resolveContext) { !isFinal(it.qualifiedName) } - .asSequence() - .mapNotNull { it.element } - .any { it is PyTargetExpression && isFinal(it.qualifiedName) } - .let { - if (it) registerProblem(value, "If assigned value is omitted, there should be an explicit type argument to 'Final'") - } + node.annotation?.value.takeIf(this::resolvesToFinal).let { + registerProblem(it, "If assigned value is omitted, there should be an explicit type argument to 'Final'") } } } + override fun visitPyNamedParameter(node: PyNamedParameter) { + super.visitPyNamedParameter(node) + + val typeHint = typeHintAsExpression(node) + if (resolvesToFinal(if (typeHint is PySubscriptionExpression) typeHint.operand else typeHint)) { + registerProblem(node.annotation?.value ?: node.typeComment, "'Final' could not be used in annotations for function parameters") + } + } + private fun isFinal(decoratable: PyDecoratable): Boolean { return PyKnownDecoratorUtil.getKnownDecorators(decoratable, myTypeEvalContext).any { it == TYPING_FINAL || it == TYPING_FINAL_EXT } } @@ -91,5 +100,25 @@ class PyFinalInspection : PyInspection() { private fun isFinal(qualifiedName: String?): Boolean { return qualifiedName == PyTypingTypeProvider.FINAL || qualifiedName == PyTypingTypeProvider.FINAL_EXT } + + private fun resolvesToFinal(expression: PyExpression?): Boolean { + return expression is PyReferenceExpression && + expression + .multiFollowAssignmentsChain(resolveContext) { !isFinal(it.qualifiedName) } + .asSequence() + .mapNotNull { it.element } + .any { it is PyTargetExpression && isFinal(it.qualifiedName) } + } + + private fun typeHintAsExpression(node: T): PyExpression? where T : PyAnnotationOwner, T : PyTypeCommentOwner { + val annotation = node.annotation?.value + if (annotation != null) return annotation + + val typeComment = node.typeCommentAnnotation + if (typeComment == null) return null + + val file = FileContextUtil.getContextFile(node) ?: return null + return PyUtil.createExpressionFromFragment(typeComment, file) + } } } \ 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 bf13a25a2ea6..26e125249fc0 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java @@ -123,6 +123,25 @@ public class PyFinalInspectionTest extends PyInspectionTestCase { assertSdkRootsNotParsed(currentFile); } + // PY-34945 + public void testFinalParameter() { + runWithLanguageLevel( + LanguageLevel.PYTHON35, + () -> doTestByText("from typing_extensions import Final\n" + + "\n" + + "def foo(a: Final) -> None:\n" + + " pass\n" + + "\n" + + "def bar(a, # type: Final[str]\n" + + " ):\n" + + " pass\n" + + "\n" + + "def baz(a):\n" + + " # type: (Final[int]) -> None\n" + + " pass") + ); + } + @NotNull @Override protected Class getInspectionClass() {