Raise an error about invalid order in inherited fields (PY-28506, PY-31762, PY-33165)

This commit is contained in:
Semyon Proshev
2018-12-28 12:41:07 +03:00
parent 9e8b3363a3
commit 92f9b2474b
3 changed files with 64 additions and 7 deletions
@@ -110,6 +110,7 @@ class PyDataclassInspection : PyInspection() {
PyNamedTupleInspection.inspectFieldsOrder(
node,
{ parseDataclassParameters(it, myTypeEvalContext) != null },
dataclassParameters.type == PyDataclassParameters.Type.STD,
myTypeEvalContext,
this::registerProblem,
{
@@ -20,21 +20,48 @@ class PyNamedTupleInspection : PyInspection() {
companion object {
fun inspectFieldsOrder(cls: PyClass,
ancestorsFilter: (PyClass) -> Boolean,
checkInheritedOrder: Boolean,
context: TypeEvalContext,
callback: (PsiElement, String, ProblemHighlightType) -> Unit,
fieldsFilter: (PyTargetExpression) -> Boolean = { true },
hasAssignedValue: (PyTargetExpression) -> Boolean = PyTargetExpression::hasAssignedValue) {
val fieldsProcessor = processFields(cls, fieldsFilter, hasAssignedValue)
val ancestors = cls.getAncestorClasses(context)
val ancestorsFields = ancestors.map {
when {
!ancestorsFilter(it) -> Ancestor.FILTERED
processFields(it, fieldsFilter, hasAssignedValue).fieldsWithDefaultValue.isNotEmpty() -> Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE
else -> Ancestor.HAS_NOT_FIELD_WITH_DEFAULT_VALUE
}
}
if (checkInheritedOrder) {
var seenAncestorHavingFieldWithDefaultValue: PyClass? = null
for (ancestorAndFields in ancestors.zip(ancestorsFields).asReversed()) {
if (ancestorAndFields.second == Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE) seenAncestorHavingFieldWithDefaultValue = ancestorAndFields.first
else if (ancestorAndFields.second == Ancestor.HAS_NOT_FIELD_WITH_DEFAULT_VALUE && seenAncestorHavingFieldWithDefaultValue != null) {
callback(
cls.superClassExpressionList!!,
"Inherited non-default argument(s) defined in ${ancestorAndFields.first.name} follows " +
"inherited default argument defined in ${seenAncestorHavingFieldWithDefaultValue.name}",
ProblemHighlightType.GENERIC_ERROR
)
break
}
}
}
val lastFieldWithoutDefaultValue = fieldsProcessor.lastFieldWithoutDefaultValue
if (lastFieldWithoutDefaultValue != null) {
val ancestorsHavingFieldsWithDefaultValue = cls.getAncestorClasses(context).filter {
ancestorsFilter(it) && processFields(it, fieldsFilter, hasAssignedValue).fieldsWithDefaultValue.isNotEmpty()
}
if (ancestorsHavingFieldsWithDefaultValue.isNotEmpty()) {
if (ancestorsFields.contains(Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE)) {
cls.nameIdentifier?.let { name ->
val ancestorsNames = ancestorsHavingFieldsWithDefaultValue.joinToString { "'${it.name}'" }
val ancestorsNames = ancestors
.asSequence()
.zip(ancestorsFields.asSequence())
.filter { it.second == Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE }
.joinToString { "'${it.first.name}'" }
callback(name,
"Non-default argument(s) follows default argument(s) defined in $ancestorsNames",
@@ -58,6 +85,10 @@ class PyNamedTupleInspection : PyInspection() {
return fieldsProcessor
}
private enum class Ancestor {
FILTERED, HAS_FIELD_WITH_DEFAULT_VALUE, HAS_NOT_FIELD_WITH_DEFAULT_VALUE
}
}
override fun buildVisitor(holder: ProblemsHolder,
@@ -72,7 +103,7 @@ class PyNamedTupleInspection : PyInspection() {
if (node != null &&
LanguageLevel.forElement(node).isAtLeast(LanguageLevel.PYTHON36) &&
PyNamedTupleTypeProvider.isTypingNamedTupleDirectInheritor(node, myTypeEvalContext)) {
inspectFieldsOrder(node, { false }, myTypeEvalContext, this::registerProblem)
inspectFieldsOrder(node, { false }, false, myTypeEvalContext, this::registerProblem)
}
}
}
@@ -28,3 +28,28 @@ class A3:
@dataclasses.dataclass
class B3(A3):
y1: str
@dataclasses.dataclass
class A41:
field1: int
@dataclasses.dataclass
class A42:
field2: str = "1"
@dataclasses.dataclass
class B4<error descr="Inherited non-default argument(s) defined in A41 follows inherited default argument defined in A42">(A41, A42)</error>:
pass
@attr.dataclass
class A51:
field1: int
@attr.dataclass
class A52:
field2: str = "1"
@attr.dataclass
class B5(A51, A52):
pass