diff --git a/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt b/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt index be9a1582e4b6..0d92b56764e3 100644 --- a/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyFinalInspection.kt @@ -52,6 +52,8 @@ class PyFinalInspection : PyInspection() { checkClassLevelFinalsAreInitialized(classLevelFinals, initAttributes) checkSameNameClassAndInstanceFinals(classLevelFinals, initAttributes) } + + checkOverridingInheritedFinalWithNewOne(node) } override fun visitPyFunction(node: PyFunction) { @@ -140,6 +142,28 @@ class PyFinalInspection : PyInspection() { return Pair(classLevelFinals, initAttributes) } + private fun getDeclaredClassAndInstanceFinals(cls: PyClass): Pair, Map> { + val classFinals = mutableMapOf() + val instanceFinals = mutableMapOf() + + for (classAttribute in cls.classAttributes) { + val name = classAttribute.name ?: continue + + if (isFinal(classAttribute)) { + val mapToPut = if (classAttribute.hasAssignedValue()) classFinals else instanceFinals + mapToPut[name] = classAttribute + } + } + + cls.findMethodByName(PyNames.INIT, false, myTypeEvalContext)?.let { init -> + val attributesInInit = mutableMapOf() + PyClassImpl.collectInstanceAttributes(init, attributesInInit, instanceFinals.keys) + instanceFinals += attributesInInit.filterValues { isFinal(it) } + } + + return Pair(classFinals, instanceFinals) + } + private fun checkClassLevelFinalsAreInitialized(classLevelFinals: Map, initAttributes: Map) { classLevelFinals.forEach { (name, psi) -> @@ -167,6 +191,35 @@ class PyFinalInspection : PyInspection() { } } + private fun checkOverridingInheritedFinalWithNewOne(cls: PyClass) { + val (newClassFinals, newInstanceFinals) = getDeclaredClassAndInstanceFinals(cls) + + val notRegisteredClassFinals = newClassFinals.keys.toMutableSet() + val notRegisteredInstanceFinals = newInstanceFinals.keys.toMutableSet() + if (notRegisteredClassFinals.isEmpty() && notRegisteredInstanceFinals.isEmpty()) return + + for (ancestor in cls.getAncestorClasses(myTypeEvalContext)) { + val (inheritedClassFinals, inheritedInstanceFinals) = getDeclaredClassAndInstanceFinals(ancestor) + + checkOverridingInheritedFinalWithNewOne(newClassFinals, inheritedClassFinals, ancestor.name, notRegisteredClassFinals) + checkOverridingInheritedFinalWithNewOne(newInstanceFinals, inheritedInstanceFinals, ancestor.name, notRegisteredInstanceFinals) + + if (notRegisteredClassFinals.isEmpty() && notRegisteredInstanceFinals.isEmpty()) break + } + } + + private fun checkOverridingInheritedFinalWithNewOne(newFinals: Map, + inheritedFinals: Map, + ancestorName: String?, + notRegistered: MutableSet) { + if (notRegistered.isEmpty()) return + + for (commonFinal in newFinals.keys.intersect(inheritedFinals.keys)) { + registerProblem(newFinals[commonFinal], "'$ancestorName.$commonFinal' is 'Final' and could not be overridden") + notRegistered.remove(commonFinal) + } + } + private fun checkInstanceFinalsOutsideInit(method: PyFunction) { if (PyUtil.isInit(method)) return diff --git a/python/testData/inspections/PyFinalInspection/ImportedClassFinalOverriding/a.py b/python/testData/inspections/PyFinalInspection/ImportedClassFinalOverriding/a.py new file mode 100644 index 000000000000..fa62b2da204d --- /dev/null +++ b/python/testData/inspections/PyFinalInspection/ImportedClassFinalOverriding/a.py @@ -0,0 +1,5 @@ +from typing_extensions import Final +from b import A + +class B(A): + a: Final[str] = "3" diff --git a/python/testData/inspections/PyFinalInspection/ImportedClassFinalOverriding/b.py b/python/testData/inspections/PyFinalInspection/ImportedClassFinalOverriding/b.py new file mode 100644 index 000000000000..639e612dc326 --- /dev/null +++ b/python/testData/inspections/PyFinalInspection/ImportedClassFinalOverriding/b.py @@ -0,0 +1,4 @@ +from typing_extensions import Final + +class A: + a: Final[int] = 1 \ No newline at end of file diff --git a/python/testData/inspections/PyFinalInspection/ImportedInstanceFinalOverriding/a.py b/python/testData/inspections/PyFinalInspection/ImportedInstanceFinalOverriding/a.py new file mode 100644 index 000000000000..8d08c1fa465b --- /dev/null +++ b/python/testData/inspections/PyFinalInspection/ImportedInstanceFinalOverriding/a.py @@ -0,0 +1,14 @@ +from typing_extensions import Final +from b import A + +class B(A): + def __init__(self): + super().__init__() + self.a: Final[str] = "2" + +class C(A): + a: Final[str] + + def __init__(self): + super().__init__() + self.a = "3" \ No newline at end of file diff --git a/python/testData/inspections/PyFinalInspection/ImportedInstanceFinalOverriding/b.py b/python/testData/inspections/PyFinalInspection/ImportedInstanceFinalOverriding/b.py new file mode 100644 index 000000000000..e91869e4f20c --- /dev/null +++ b/python/testData/inspections/PyFinalInspection/ImportedInstanceFinalOverriding/b.py @@ -0,0 +1,7 @@ +from typing_extensions import Final + +class A: + a: Final[int] + + def __init__(self): + self.a = 1 \ 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 a6b202d7dd00..db2b254ca8bb 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java @@ -415,6 +415,56 @@ public class PyFinalInspectionTest extends PyInspectionTestCase { ); } + // PY-34945 + public void testClassFinalOverriding() { + runWithLanguageLevel( + LanguageLevel.PYTHON36, + () -> doTestByText("from typing_extensions import Final\n" + + "\n" + + "class A:\n" + + " a: Final[int] = 1\n" + + "\n" + + "class B(A):\n" + + " a: Final[str] = \"3\"\n") + ); + } + + // PY-34945 + public void testImportedClassFinalOverriding() { + runWithLanguageLevel(LanguageLevel.PYTHON36, this::doMultiFileTest); + } + + // PY-34945 + public void testInstanceFinalOverriding() { + runWithLanguageLevel( + LanguageLevel.PYTHON36, + () -> doTestByText("from typing_extensions import Final\n" + + "\n" + + "class A:\n" + + " a: Final[int]\n" + + "\n" + + " def __init__(self):\n" + + " self.a = 1\n" + + "\n" + + "class B(A):\n" + + " def __init__(self):\n" + + " super().__init__()\n" + + " self.a: Final[str] = \"2\"\n" + + "\n" + + "class C(A):\n" + + " a: Final[str]\n" + + "\n" + + " def __init__(self):\n" + + " super().__init__()\n" + + " self.a = \"3\"") + ); + } + + // PY-34945 + public void testImportedInstanceFinalOverriding() { + runWithLanguageLevel(LanguageLevel.PYTHON36, this::doMultiFileTest); + } + @NotNull @Override protected Class getInspectionClass() {