Raise a warning on Finals overriding (PEP 591) (PY-34945)

GitOrigin-RevId: 53c741c0ba2467b0fde342ec7e95bca952651522
This commit is contained in:
Semyon Proshev
2019-07-02 06:52:16 +03:00
committed by intellij-monorepo-bot
parent 0509475adc
commit b70415af67
6 changed files with 133 additions and 0 deletions
@@ -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<String, PyTargetExpression>, Map<String, PyTargetExpression>> {
val classFinals = mutableMapOf<String, PyTargetExpression>()
val instanceFinals = mutableMapOf<String, PyTargetExpression>()
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<String, PyTargetExpression>()
PyClassImpl.collectInstanceAttributes(init, attributesInInit, instanceFinals.keys)
instanceFinals += attributesInInit.filterValues { isFinal(it) }
}
return Pair(classFinals, instanceFinals)
}
private fun checkClassLevelFinalsAreInitialized(classLevelFinals: Map<String?, PyTargetExpression>,
initAttributes: Map<String, PyTargetExpression>) {
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<String, PyTargetExpression>,
inheritedFinals: Map<String, PyTargetExpression>,
ancestorName: String?,
notRegistered: MutableSet<String>) {
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
@@ -0,0 +1,5 @@
from typing_extensions import Final
from b import A
class B(A):
<warning descr="'A.a' is 'Final' and could not be overridden">a</warning>: Final[str] = "3"
@@ -0,0 +1,4 @@
from typing_extensions import Final
class A:
a: Final[int] = 1
@@ -0,0 +1,14 @@
from typing_extensions import Final
from b import A
class B(A):
def __init__(self):
super().__init__()
<warning descr="'A.a' is 'Final' and could not be overridden">self.a</warning>: Final[str] = "2"
class C(A):
<warning descr="'A.a' is 'Final' and could not be overridden">a</warning>: Final[str]
def __init__(self):
super().__init__()
self.a = "3"
@@ -0,0 +1,7 @@
from typing_extensions import Final
class A:
a: Final[int]
def __init__(self):
self.a = 1
@@ -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" +
" <warning descr=\"'A.a' is 'Final' and could not be overridden\">a</warning>: 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" +
" <warning descr=\"'A.a' is 'Final' and could not be overridden\">self.a</warning>: Final[str] = \"2\"\n" +
"\n" +
"class C(A):\n" +
" <warning descr=\"'A.a' is 'Final' and could not be overridden\">a</warning>: 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<? extends PyInspection> getInspectionClass() {