diff --git a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt index 4bf153c74713..6490eceb3419 100644 --- a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt @@ -110,6 +110,7 @@ class PyDataclassInspection : PyInspection() { PyNamedTupleInspection.inspectFieldsOrder( node, { parseDataclassParameters(it, myTypeEvalContext) != null }, + dataclassParameters.type == PyDataclassParameters.Type.STD, myTypeEvalContext, this::registerProblem, { diff --git a/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt b/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt index 604f64e586c7..7cddc848860e 100644 --- a/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt @@ -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) } } } diff --git a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritance.py b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritance.py index 481fb4581ac6..f96efae859bb 100644 --- a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritance.py +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritance.py @@ -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(A41, A42): + pass + +@attr.dataclass +class A51: + field1: int + +@attr.dataclass +class A52: + field2: str = "1" + +@attr.dataclass +class B5(A51, A52): + pass +