diff --git a/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml b/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml index 0743e9e1dace..8bb1c12111b7 100644 --- a/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml +++ b/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml @@ -128,7 +128,7 @@ - + diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index d8a5c70ccb83..fa4f31eb719a 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -442,7 +442,6 @@ INSP.pep8.naming=PEP 8 naming convention violation INSP.pep8.naming.excluded.base.classes=Excluded base classes INSP.pep8.naming.ignored.errors=Ignored errors INSP.shadowing.builtins=Shadowing built-ins -INSP.dataclass.definition.and.usages=Dataclass definition and usages INSP.dunder.slots=Definition of __slots__ in a class INSP.final.classes.methods.and.variables=Final classes, methods and variables INSP.global.undefined=Global variable is undefined at the module level @@ -868,6 +867,39 @@ INSP.class.$0.superclass.$1.already.caught=''{0}'', superclass of exception clas INSP.NAME.global.undefined=Global variable is undefined at the module level INSP.NAME.global.$0.undefined=Global variable ''{0}'' is undefined at the module level +#PyDataclassInspection +INSP.NAME.dataclass.definition.and.usages=Dataclass definition and usages +INSP.dataclasses.operator.not.supported.between.instances.of.class=''{0}'' not supported between instances of ''{1}'' +INSP.dataclasses.operator.not.supported.between.instances.of.classes=''{0}'' not supported between instances of ''{1}'' and ''{2}'' +INSP.dataclasses.object.could.have.no.attribute.because.it.declared.as.init.only=''{0}'' object could have no attribute ''{1}'' because it is declared as init-only +INSP.dataclasses.object.attribute.read.only=''{0}'' object attribute ''{1}'' is read-only +INSP.dataclasses.eq.must.be.true.if.order.true='eq' must be true if 'order' is true +INSP.dataclasses.argument.ignored.if.class.already.defines.method=''{0}'' is ignored if the class already defines ''{1}'' method +INSP.dataclasses.order.argument.should.be.false.if.class.defines.one.of.order.methods='order' should be false if the class defines one of order methods +INSP.dataclasses.frozen.attribute.should.be.false.if.class.defines.setattr.or.delattr='frozen' should be false if the class defines '__setattr__' or '__delattr__' +INSP.dataclasses.unsafe.hash.attribute.should.be.false.if.class.defines.hash='unsafe_hash' should be false if the class defines '__hash__' +INSP.dataclasses.frozen.dataclasses.can.not.inherit.non.frozen.one=Frozen dataclasses can not inherit non-frozen one and vice versa +INSP.dataclasses.method.is.ignored.if.class.already.defines.parameter=''{0}'' is ignored if the class already defines ''{1}'' parameter +INSP.dataclasses.hash.ignored.if.class.already.defines.cmp.or.order.or.frozen.parameters='__hash__' is ignored if the class already defines 'cmp/order' and 'frozen' parameters +INSP.dataclasses.mutable.attribute.default.not.allowed.use.default.factory=Mutable default ''{0}'' is not allowed. Use ''default_factory'' +INSP.dataclasses.attribute.default.is.set.using.attr.ib=A default is set using 'attr.ib()' +INSP.dataclasses.attribute.default.set.using.method=A default is set using ''{0}'' +INSP.dataclasses.method.should.take.only.n.parameter=''{0}'' should take only {1} {1,choice,1#parameter|2#parameters} +INSP.dataclasses.attribute.lacks.type.annotation=Attribute ''{0}'' lacks a type annotation +INSP.dataclasses.cannot.specify.both.default.and.factory=Cannot specify both 'default' and 'factory' +INSP.dataclasses.attribute.useless.until.post.init.declared=Attribute ''{0}'' is useless until ''__post_init__'' is declared +INSP.dataclasses.field.cannot.have.default.factory=Field cannot have a default factory +INSP.dataclasses.cannot.specify.both.default.and.default.factory=Cannot specify both 'default' and 'default_factory' +INSP.dataclasses.post.init.would.not.be.called.until.init.parameter.set.to.true='__post_init__' would not be called until 'init' parameter is set to True +INSP.dataclasses.post.init.should.take.all.init.only.variables.including.inherited.in.same.order.they.defined='__post_init__' should take all init-only variables (incl. inherited) in the same order as they are defined +INSP.dataclasses.post.init.should.take.all.init.only.variables.in.same.order.they.defined='__post_init__' should take all init-only variables in the same order as they are defined +INSP.dataclasses.attrs.post.init.would.not.be.called.until.init.parameter.set.to.true='__attrs_post_init__' would not be called until 'init' parameter is set to True +INSP.dataclasses.attrs.post.init.should.not.take.any.parameters.except.self='__attrs_post_init__' should not take any parameters except 'self' +INSP.dataclasses.method.should.be.called.on.dataclass.instances.or.types=''{0}'' method should be called on dataclass instances or types +INSP.dataclasses.method.should.be.called.on.dataclass.instances=''{0}'' method should be called on dataclass instances +INSP.dataclasses.method.should.be.called.on.attrs.instances=''{0}'' method should be called on attrs instances +INSP.dataclasses.method.should.be.called.on.attrs.types=''{0}'' method should be called on attrs types + # PyHighlightingAnnotator INSP.python.trailing.suffix.not.support=Python does not support a trailing ''{0}'' diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt index 03f5e81b5fff..5719e70ebad5 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt @@ -11,6 +11,7 @@ import com.intellij.psi.PsiNameIdentifierOwner import com.intellij.util.containers.ContainerUtil import com.intellij.util.containers.isNullOrEmpty import com.jetbrains.python.PyNames +import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.codeInsight.* import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider import com.jetbrains.python.psi.* @@ -162,7 +163,7 @@ class PyDataclassInspection : PyInspection() { if (leftClass == rightClass) { if (leftOrder == ClassOrder.DC_UNORDERED && rightOrder != ClassOrder.MANUALLY) { registerProblem(node.psiOperator, - "'$leftOperator' not supported between instances of '${leftClass.name}'", + PyPsiBundle.message("INSP.dataclasses.operator.not.supported.between.instances.of.class", leftOperator, leftClass.name), ProblemHighlightType.GENERIC_ERROR) } } @@ -176,7 +177,7 @@ class PyDataclassInspection : PyInspection() { rightClass.isSubclass(leftClass, myTypeEvalContext)) return // attrs allows to compare ancestor and its subclass registerProblem(node.psiOperator, - "'$leftOperator' not supported between instances of '${leftClass.name}' and '${rightClass.name}'", + PyPsiBundle.message("INSP.dataclasses.operator.not.supported.between.instances.of.classes", leftOperator, leftClass.name, rightClass.name), ProblemHighlightType.GENERIC_ERROR) } } @@ -223,7 +224,7 @@ class PyDataclassInspection : PyInspection() { if (resolved.isNotEmpty() && resolved.asSequence().map { it.element }.all { it is PyTargetExpression && isInitVar(it) }) { registerProblem(node.lastChild, - "'${cls.name}' object could have no attribute '${node.name}' because it is declared as init-only", + PyPsiBundle.message("INSP.dataclasses.object.could.have.no.attribute.because.it.declared.as.init.only", cls.name, node.name), ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } } @@ -237,7 +238,7 @@ class PyDataclassInspection : PyInspection() { .mapNotNull { parseDataclassParameters(it, myTypeEvalContext) } .any { it.frozen }) { registerProblem(expression, - "'${cls.name}' object attribute '${expression.name}' is read-only", + PyPsiBundle.message("INSP.dataclasses.object.attribute.read.only", cls.name, expression.name), ProblemHighlightType.GENERIC_ERROR) } } @@ -276,7 +277,7 @@ class PyDataclassInspection : PyInspection() { private fun processDataclassParameters(cls: PyClass, dataclassParameters: PyDataclassParameters) { if (!dataclassParameters.eq && dataclassParameters.order) { - registerProblem(dataclassParameters.eqArgument, "'eq' must be true if 'order' is true", ProblemHighlightType.GENERIC_ERROR) + registerProblem(dataclassParameters.eqArgument, PyPsiBundle.message("INSP.dataclasses.eq.must.be.true.if.order.true"), ProblemHighlightType.GENERIC_ERROR) } var initMethodExists = false @@ -316,25 +317,25 @@ class PyDataclassInspection : PyInspection() { useless.forEach { registerProblem(it.first, - "'${it.second}' is ignored if the class already defines '${it.third}' method", + PyPsiBundle.message("INSP.dataclasses.argument.ignored.if.class.already.defines.method", it.second, it.third), ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } if (dataclassParameters.order && orderMethodsExist) { registerProblem(dataclassParameters.orderArgument, - "'order' should be false if the class defines one of order methods", + PyPsiBundle.message("INSP.dataclasses.order.argument.should.be.false.if.class.defines.one.of.order.methods"), ProblemHighlightType.GENERIC_ERROR) } if (dataclassParameters.frozen && mutatingMethodsExist) { registerProblem(dataclassParameters.frozenArgument, - "'frozen' should be false if the class defines '__setattr__' or '__delattr__'", + PyPsiBundle.message("INSP.dataclasses.frozen.attribute.should.be.false.if.class.defines.setattr.or.delattr"), ProblemHighlightType.GENERIC_ERROR) } if (dataclassParameters.unsafeHash && hashMethodExists) { registerProblem(dataclassParameters.unsafeHashArgument, - "'unsafe_hash' should be false if the class defines '${PyNames.HASH}'", + PyPsiBundle.message("INSP.dataclasses.unsafe.hash.attribute.should.be.false.if.class.defines.hash"), ProblemHighlightType.GENERIC_ERROR) } @@ -347,7 +348,7 @@ class PyDataclassInspection : PyInspection() { } else if (frozenInHierarchy != currentFrozen) { registerProblem(dataclassParameters.frozenArgument ?: cls.nameIdentifier, - "Frozen dataclasses can not inherit non-frozen one and vice versa", + PyPsiBundle.message("INSP.dataclasses.frozen.dataclasses.can.not.inherit.non.frozen.one"), ProblemHighlightType.GENERIC_ERROR) } } @@ -405,14 +406,14 @@ class PyDataclassInspection : PyInspection() { problems.forEach { it.first?.apply { registerProblem(nameIdentifier, - "'$name' is ignored if the class already defines '${it.second}' parameter", + PyPsiBundle.message("INSP.dataclasses.method.is.ignored.if.class.already.defines.parameter", name, it.second), ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } } if (dataclassParameters.order && dataclassParameters.frozen && hashMethod != null) { registerProblem(hashMethod?.nameIdentifier, - "'${PyNames.HASH}' is ignored if the class already defines 'cmp/order' and 'frozen' parameters", + PyPsiBundle.message("INSP.dataclasses.hash.ignored.if.class.already.defines.cmp.or.order.or.frozen.parameters"), ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } } @@ -423,7 +424,7 @@ class PyDataclassInspection : PyInspection() { val value = field.findAssignedValue() if (PyUtil.isForbiddenMutableDefault(value, myTypeEvalContext)) { registerProblem(value, - "Mutable default '${value?.text}' is not allowed. Use 'default_factory'", + PyPsiBundle.message("INSP.dataclasses.mutable.attribute.default.not.allowed.use.default.factory", value?.text), ProblemHighlightType.GENERIC_ERROR) } } @@ -448,7 +449,9 @@ class PyDataclassInspection : PyInspection() { val stub = PyDataclassFieldStubImpl.create(attribute) if (stub != null && (stub.hasDefault() || stub.hasDefaultFactory())) { - registerProblem(method.nameIdentifier, "A default is set using 'attr.ib()'", ProblemHighlightType.GENERIC_ERROR) + registerProblem(method.nameIdentifier, + PyPsiBundle.message("INSP.dataclasses.attribute.default.is.set.using.attr.ib"), + ProblemHighlightType.GENERIC_ERROR) } } } @@ -461,7 +464,9 @@ class PyDataclassInspection : PyInspection() { sameAttrInitializers .asSequence() .drop(1) - .forEach { registerProblem(it.nameIdentifier, "A default is set using '${first.name}'", ProblemHighlightType.GENERIC_ERROR) } + .forEach { registerProblem(it.nameIdentifier, + PyPsiBundle.message("INSP.dataclasses.attribute.default.set.using.method", first.name), + ProblemHighlightType.GENERIC_ERROR) } } } @@ -485,8 +490,7 @@ class PyDataclassInspection : PyInspection() { val actualParameters = method.parameterList if (actualParameters.parameters.size != expectedParameters) { - val message = "'${method.name}' should take only $expectedParameters parameter" + - if (expectedParameters > 1) "s" else "" + val message = PyPsiBundle.message("INSP.dataclasses.method.should.take.only.n.parameter", method.name, expectedParameters) registerProblem(actualParameters, message, ProblemHighlightType.GENERIC_ERROR) } @@ -505,7 +509,8 @@ class PyDataclassInspection : PyInspection() { PyEvaluator.evaluateAsBoolean(PyUtil.peelArgument(dataclassParameters.others["auto_attribs"]), false)) { cls.processClassLevelDeclarations { element, _ -> if (element is PyTargetExpression && element.annotation == null && PyDataclassFieldStubImpl.create(element) != null) { - registerProblem(element, "Attribute '${element.name}' lacks a type annotation", ProblemHighlightType.GENERIC_ERROR) + registerProblem(element, PyPsiBundle.message("INSP.dataclasses.attribute.lacks.type.annotation", element.name), + ProblemHighlightType.GENERIC_ERROR) } true @@ -522,7 +527,8 @@ class PyDataclassInspection : PyInspection() { if (call != null && stub != null) { if (stub.hasDefaultFactory()) { if (stub.hasDefault()) { - registerProblem(call.argumentList, "Cannot specify both 'default' and 'factory'", ProblemHighlightType.GENERIC_ERROR) + registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.factory"), + ProblemHighlightType.GENERIC_ERROR) } else { // at least covers the following case: `attr.ib(default=attr.Factory(...), factory=...)` @@ -531,7 +537,8 @@ class PyDataclassInspection : PyInspection() { val factory = call.getKeywordArgument("factory") if (default != null && factory != null && !resolvesToOmittedDefault(default, PyDataclassParameters.PredefinedType.ATTRS)) { - registerProblem(call.argumentList, "Cannot specify both 'default' and 'factory'", ProblemHighlightType.GENERIC_ERROR) + registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.factory"), + ProblemHighlightType.GENERIC_ERROR) } } } @@ -546,7 +553,7 @@ class PyDataclassInspection : PyInspection() { if (isInitVar(field)) { if (postInit == null) { registerProblem(field, - "Attribute '${field.name}' is useless until '$DUNDER_POST_INIT' is declared", + PyPsiBundle.message("INSP.dataclasses.attribute.useless.until.post.init.declared", field.name), ProblemHighlightType.LIKE_UNUSED_SYMBOL) } @@ -563,12 +570,13 @@ class PyDataclassInspection : PyInspection() { if (PyTypingTypeProvider.isClassVar(field, myTypeEvalContext) || isInitVar(field)) { if (fieldStub.hasDefaultFactory()) { registerProblem(call.getKeywordArgument("default_factory"), - "Field cannot have a default factory", + PyPsiBundle.message("INSP.dataclasses.field.cannot.have.default.factory"), ProblemHighlightType.GENERIC_ERROR) } } else if (fieldStub.hasDefault() && fieldStub.hasDefaultFactory()) { - registerProblem(call.argumentList, "Cannot specify both 'default' and 'default_factory'", ProblemHighlightType.GENERIC_ERROR) + registerProblem(call.argumentList, PyPsiBundle.message("INSP.dataclasses.cannot.specify.both.default.and.default.factory"), + ProblemHighlightType.GENERIC_ERROR) } } @@ -578,7 +586,7 @@ class PyDataclassInspection : PyInspection() { localInitVars: List) { if (!dataclassParameters.init) { registerProblem(postInit.nameIdentifier, - "'$DUNDER_POST_INIT' would not be called until 'init' parameter is set to True", + PyPsiBundle.message("INSP.dataclasses.post.init.would.not.be.called.until.init.parameter.set.to.true"), ProblemHighlightType.LIKE_UNUSED_SYMBOL) return @@ -602,10 +610,14 @@ class PyDataclassInspection : PyInspection() { val implicitParameters = postInit.getParameters(myTypeEvalContext) val parameters = if (implicitParameters.isEmpty()) emptyList() else ContainerUtil.subList(implicitParameters, 1) - val message = "'$DUNDER_POST_INIT' " + - "should take all init-only variables" + - "${if (allInitVars.size != localInitVars.size) " (incl. inherited)" else ""} " + - "in the same order as they are defined" + + val message = if (allInitVars.size != localInitVars.size) { + PyPsiBundle.message("INSP.dataclasses.post.init.should.take.all.init.only.variables.including.inherited.in.same.order.they.defined") + } + else { + PyPsiBundle.message("INSP.dataclasses.post.init.should.take.all.init.only.variables.in.same.order.they.defined") + } + if (parameters.size != allInitVars.size) { registerProblem(postInit.parameterList, message, ProblemHighlightType.GENERIC_ERROR) @@ -622,13 +634,13 @@ class PyDataclassInspection : PyInspection() { private fun processAttrsPostInitDefinition(postInit: PyFunction, dataclassParameters: PyDataclassParameters) { if (!dataclassParameters.init) { registerProblem(postInit.nameIdentifier, - "'$DUNDER_ATTRS_POST_INIT' would not be called until 'init' parameter is set to True", + PyPsiBundle.message("INSP.dataclasses.attrs.post.init.would.not.be.called.until.init.parameter.set.to.true"), ProblemHighlightType.LIKE_UNUSED_SYMBOL) } if (postInit.getParameters(myTypeEvalContext).size != 1) { registerProblem(postInit.parameterList, - "'$DUNDER_ATTRS_POST_INIT' should not take any parameters except '${PyNames.CANONICAL_SELF}'", + PyPsiBundle.message("INSP.dataclasses.attrs.post.init.should.not.take.any.parameters.except.self"), ProblemHighlightType.GENERIC_ERROR) } } @@ -641,7 +653,12 @@ class PyDataclassInspection : PyInspection() { val type = myTypeEvalContext.getType(argument) val allowSubclass = calleeQName != "dataclasses.asdict" if (!isExpectedDataclass(type, PyDataclassParameters.PredefinedType.STD, allowDefinition, true, allowSubclass)) { - val message = "'$calleeQName' method should be called on dataclass instances" + if (allowDefinition) " or types" else "" + val message = if (allowDefinition) { + PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.dataclass.instances.or.types", calleeQName) + } + else { + PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.dataclass.instances", calleeQName) + } registerProblem(argument, message) } @@ -654,7 +671,12 @@ class PyDataclassInspection : PyInspection() { val type = myTypeEvalContext.getType(argument) if (!isExpectedDataclass(type, PyDataclassParameters.PredefinedType.ATTRS, !instance, instance, true)) { - val message = "'$calleeQName' method should be called on attrs " + if (instance) "instances" else "types" + val message = if (instance) { + PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.attrs.instances", calleeQName) + } + else { + PyPsiBundle.message("INSP.dataclasses.method.should.be.called.on.attrs.types", calleeQName) + } registerProblem(argument, message) }