diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 9f1019527a68..498659a37bf0 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1002,9 +1002,7 @@ INSP.dunder.slots.class.object.attribute.read.only=''{0}'' object attribute ''{1 # PyFinalInspection INSP.NAME.final.classes.methods.and.variables=Invalid usages of final classes, methods, and variables INSP.final.super.classes.are.marked.as.final.and.should.not.be.subclassed={0} {1,choice,1#is|2#are} marked as ''@final'' and should not be subclassed -INSP.final.final.should.be.placed.on.first.overload='@final' should be placed on the first overload INSP.final.method.marked.as.final.should.not.be.overridden=''{0}'' is marked as ''@final'' and should not be overridden -INSP.final.final.should.be.placed.on.implementation='@final' should be placed on the implementation INSP.final.final.could.not.be.mixed.with.abstract.decorators='Final' could not be mixed with abstract decorators INSP.final.final.class.could.not.contain.abstract.methods='Final' class could not contain abstract methods INSP.final.no.need.to.mark.method.in.final.class.as.final=No need to mark method in 'Final' class as '@final' @@ -1052,7 +1050,9 @@ INSP.overloads.at.least.two.overload.decorated.functions.must.be.present=At leas INSP.overloads.use.staticmethod.inconsistently=Overloads use @staticmethod inconsistently INSP.overloads.use.classmethod.inconsistently=Overloads use @classmethod inconsistently INSP.overloads.override.should.be.placed.on.the.implementation='@override' should be placed on the implementation +INSP.overloads.final.should.be.placed.on.the.implementation='@final' should be placed on the implementation INSP.overloads.override.should.be.placed.only.on.the.first.overload='@override' should be placed only on the first overload +INSP.overloads.final.should.be.placed.only.on.the.first.overload='@final' should be placed only on the first overload # PyOverridesInspection INSP.NAME.invalid.usages.of.override.decorator=Invalid usages of @override decorator diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt index a78c5706826e..dd3b27a12b59 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt @@ -46,21 +46,7 @@ class PyFinalInspection : PyInspection() { superClassList, finalSuperClasses.size)) } - if (PyiUtil.isInsideStub(node)) { - val visitedNames = mutableSetOf() - - node.visitMethods( - { m -> - if (!visitedNames.add(m.name) && isFinal(m)) { - registerProblem(m.nameIdentifier, PyPsiBundle.message("INSP.final.final.should.be.placed.on.first.overload")) - } - true - }, - false, - myTypeEvalContext - ) - } - else { + if (!PyiUtil.isInsideStub(node)) { val (classLevelFinals, initAttributes) = getClassLevelFinalsAndInitAttributes(node) if (!isDataclass(node)) { checkClassLevelFinalsAreInitialized(classLevelFinals, initAttributes) @@ -93,10 +79,6 @@ class PyFinalInspection : PyInspection() { } } if (!PyiUtil.isInsideStub(node)) { - if (isFinal(node) && PyiUtil.isOverload(node, myTypeEvalContext)) { - registerProblem(node.nameIdentifier, PyPsiBundle.message("INSP.final.final.should.be.placed.on.implementation")) - } - checkInstanceFinalsOutsideInit(node) } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt index ebf501deee4b..f51e33c4667f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt @@ -11,6 +11,7 @@ import com.intellij.util.containers.tail import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.ast.PyAstFunction import com.jetbrains.python.codeInsight.controlflow.ScopeOwner +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider import com.jetbrains.python.codeInsight.typing.isProtocol import com.jetbrains.python.psi.* import com.jetbrains.python.psi.impl.PyClassImpl @@ -61,7 +62,7 @@ class PyOverloadsInspection : PyInspection() { checkClassMethodAndStaticMethodConsistency(overloads, implementation) - checkOverride(overloads, implementation) + checkOverrideAndFinal(overloads, implementation) var requiresImplementation = true if (owner.containingFile is PyiFile) { @@ -118,13 +119,17 @@ class PyOverloadsInspection : PyInspection() { } } - private fun checkOverride(overloads: List, implementation: PyFunction?) { + private fun checkOverrideAndFinal(overloads: List, implementation: PyFunction?) { if (implementation == null) { for (overload in overloads.tail()) { if (isOverride(overload)) { registerProblem(overload.nameIdentifier, PyPsiBundle.message("INSP.overloads.override.should.be.placed.only.on.the.first.overload")) } + if (PyTypingTypeProvider.isFinal(overload, myTypeEvalContext)) { + registerProblem(overload.nameIdentifier, + PyPsiBundle.message("INSP.overloads.final.should.be.placed.only.on.the.first.overload")) + } } } else { @@ -133,6 +138,10 @@ class PyOverloadsInspection : PyInspection() { registerProblem(overload.nameIdentifier, PyPsiBundle.message("INSP.overloads.override.should.be.placed.on.the.implementation")) } + if (PyTypingTypeProvider.isFinal(overload, myTypeEvalContext)) { + registerProblem(overload.nameIdentifier, + PyPsiBundle.message("INSP.overloads.final.should.be.placed.on.the.implementation")) + } } } } diff --git a/python/testData/inspections/PyFinalInspection/overloadedFinalMethodInStub.pyi b/python/testData/inspections/PyFinalInspection/overloadedFinalMethodInStub.pyi deleted file mode 100644 index a6cbeefd192d..000000000000 --- a/python/testData/inspections/PyFinalInspection/overloadedFinalMethodInStub.pyi +++ /dev/null @@ -1,18 +0,0 @@ -from typing import overload -from typing_extensions import final - -class A: - @final - @overload - def foo(self, a: int) -> int: ... - - @overload - def foo(self, a: str) -> str: ... - -class B: - @overload - def foo(self, a: int) -> int: ... - - @final - @overload - def foo(self, a: str) -> str: ... diff --git a/python/testData/inspections/PyOverloadsInspection/finalMethods.py b/python/testData/inspections/PyOverloadsInspection/finalMethods.py new file mode 100644 index 000000000000..10a97d64f5f6 --- /dev/null +++ b/python/testData/inspections/PyOverloadsInspection/finalMethods.py @@ -0,0 +1,23 @@ +from typing import final, overload + + +class A: + @overload + def foo(self, a: int) -> int: ... + + @overload + def foo(self, a: str) -> str: ... + + @final + def foo(self, a): + pass + + @final + @overload + def bar(self, a: int) -> int: ... + + @overload + def bar(self, a: str) -> str: ... + + def bar(self, a): + pass diff --git a/python/testData/inspections/PyOverloadsInspection/finalMethods.pyi b/python/testData/inspections/PyOverloadsInspection/finalMethods.pyi new file mode 100644 index 000000000000..fbc8eafe67c2 --- /dev/null +++ b/python/testData/inspections/PyOverloadsInspection/finalMethods.pyi @@ -0,0 +1,16 @@ +from typing import final, overload + +class A: + @final + @overload + def foo(self, a: int) -> int: ... + + @overload + def foo(self, a: str) -> str: ... + + @overload + def bar(self, a: int) -> int: ... + + @final + @overload + def bar(self, a: str) -> str: ... diff --git a/python/testData/inspections/PyOverloadsInspection/overridenMethods.py b/python/testData/inspections/PyOverloadsInspection/overriddenMethods.py similarity index 100% rename from python/testData/inspections/PyOverloadsInspection/overridenMethods.py rename to python/testData/inspections/PyOverloadsInspection/overriddenMethods.py diff --git a/python/testData/inspections/PyOverloadsInspection/overriddenMethods.pyi b/python/testData/inspections/PyOverloadsInspection/overriddenMethods.pyi new file mode 100644 index 000000000000..b89e4253e3a3 --- /dev/null +++ b/python/testData/inspections/PyOverloadsInspection/overriddenMethods.pyi @@ -0,0 +1,16 @@ +from typing import override, overload + +class A: + @override + @overload + def foo(self, a: int) -> int: ... + + @overload + def foo(self, a: str) -> str: ... + + @overload + def bar(self, a: int) -> int: ... + + @override + @overload + def bar(self, a: str) -> str: ... diff --git a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java index 957798a567e6..fa82131a67a1 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java @@ -233,46 +233,6 @@ public class PyFinalInspectionTest extends PyInspectionTestCase { assertSdkRootsNotParsed(currentFile); } - // PY-34945 - public void testOverloadedFinalMethod() { - runWithLanguageLevel( - LanguageLevel.PYTHON35, - () -> doTestByText(""" - from typing import overload - from typing_extensions import final - - class A: - @overload - def foo(self, a: int) -> int: ... - - @overload - def foo(self, a: str) -> str: ... - - @final - def foo(self, a): - pass - - class B: - @final - @overload - def foo(self, a: int) -> int: ... - - @overload - def foo(self, a: str) -> str: ... - - def foo(self, a): - pass - """) - ); - } - - // PY-34945 - public void testOverloadedFinalMethodInStub() { - final PsiFile currentFile = myFixture.configureByFile(getTestFilePath() + "i"); - configureInspection(); - assertSdkRootsNotParsed(currentFile); - } - // PY-34945 public void testFinalParameter() { runWithLanguageLevel( diff --git a/python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java index 36be4e7003b2..a902a3228d87 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java @@ -48,7 +48,11 @@ public class PyOverloadsInspectionTest extends PyInspectionTestCase { doTest(); } - public void testOverridenMethods() { + public void testOverriddenMethods() { + doTest(); + } + + public void testFinalMethods() { doTest(); } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyiOverloadsInspectionTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyiOverloadsInspectionTest.kt index fd1adc4c07d4..580fa8642209 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyiOverloadsInspectionTest.kt +++ b/python/testSrc/com/jetbrains/python/inspections/PyiOverloadsInspectionTest.kt @@ -8,6 +8,14 @@ class PyiOverloadsInspectionTest : PyInspectionTestCase() { doTest() } + fun testOverriddenMethods() { + doTest() + } + + fun testFinalMethods() { + doTest() + } + override fun getTestFilePath(): String { return "$testCaseDirectory${getTestName(isLowerCaseTestFile)}.pyi" }