diff --git a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt index 1562bddaa9cf..5cc044a8cfb8 100644 --- a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt @@ -90,6 +90,7 @@ class PyDataclassInspection : PyInspection() { ?.also { processAttrsPostInitDefinition(it, dataclassParameters) } processAttrsDefaultThroughDecorator(node) + processAttrsInitializersAndValidators(node) } PyNamedTupleInspection.inspectFieldsOrder( @@ -361,6 +362,41 @@ class PyDataclassInspection : PyInspection() { } } + private fun processAttrsInitializersAndValidators(cls: PyClass) { + cls.visitMethods( + { method -> + val decorators = method.decoratorList?.decorators + + if (decorators != null) { + decorators + .asSequence() + .mapNotNull { it.qualifiedName } + .filter { it.componentCount == 2 } + .mapNotNull { it.lastComponent } + .forEach { + val expectedParameters = when (it) { + "default" -> 1 + "validator" -> 3 + else -> return@forEach + } + + val actualParameters = method.parameterList + if (actualParameters.parameters.size != expectedParameters) { + val message = "'${method.name}' should take only $expectedParameters parameter" + + if (expectedParameters > 1) "s" else "" + + registerProblem(actualParameters, message, ProblemHighlightType.GENERIC_ERROR) + } + } + } + + true + }, + false, + myTypeEvalContext + ) + } + private fun processAsInitVar(field: PyTargetExpression, postInit: PyFunction?): PyTargetExpression? { if (isInitVar(field)) { if (postInit == null) { diff --git a/python/testData/inspections/PyDataclassInspection/attrsInitializersAndValidators.py b/python/testData/inspections/PyDataclassInspection/attrsInitializersAndValidators.py new file mode 100644 index 000000000000..6920342366a8 --- /dev/null +++ b/python/testData/inspections/PyDataclassInspection/attrsInitializersAndValidators.py @@ -0,0 +1,27 @@ +import attr + + +@attr.s +class A: + x = attr.ib() + + @x.default + def init_x1(self): + return 10 + + @x.validator + def check_x1(self, attribute, value): + pass + + +@attr.s +class A: + x = attr.ib() + + @x.default + def init_x2(self, attribute, value): + return 10 + + @x.validator + def check_x2(self): + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java index f40b2ad47fe5..fa97b88cd5e2 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java @@ -161,6 +161,11 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-26354 + public void testAttrsInitializersAndValidators() { + doTest(); + } + @Override protected void doTest() { runWithLanguageLevel(