PY-76841 Conformance test failure: dataclasses_postinit.py

__post_init__ parameters have to match InitVars type not name

GitOrigin-RevId: 0e1e4a9c3a50eee84cd724b5a3f8c5e894e90bdd
This commit is contained in:
Petr
2024-12-11 20:51:44 +00:00
committed by intellij-monorepo-bot
parent 14a150d3f1
commit 5486d93839
3 changed files with 60 additions and 34 deletions
@@ -969,6 +969,7 @@ INSP.dataclasses.method.should.be.called.on.dataclass.instances.or.types=''{0}''
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
INSP.dataclasses.expected.type.got.type.instead=Expected type ''{0}'', got ''{1}'' instead
# PyDeprecationInspection
INSP.NAME.deprecated.function.class.or.module=Deprecated function, class, or module
@@ -8,13 +8,14 @@ import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.containers.tailOrEmpty
import com.jetbrains.python.PyNames
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.*
import com.jetbrains.python.codeInsight.PyDataclassNames.Attrs
import com.jetbrains.python.codeInsight.PyDataclassNames.Dataclasses
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.documentation.PythonDocumentationProvider
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.ParamHelper
import com.jetbrains.python.psi.impl.PyCallExpressionHelper
@@ -74,13 +75,13 @@ class PyDataclassInspection : PyInspection() {
processDataclassParameters(node, dataclassParameters)
val postInit = node.findMethodByName(Dataclasses.DUNDER_POST_INIT, false, myTypeEvalContext)
val localInitVars = mutableListOf<PyTargetExpression>()
val localInitVars = mutableListOf<PyType?>()
node.processClassLevelDeclarations { element, _ ->
if (element is PyTargetExpression) {
if (!PyTypingTypeProvider.isClassVar(element, myTypeEvalContext)) {
processDefaultFieldValue(element)
processAsInitVar(element, postInit)?.let { localInitVars.add(it) }
processAsInitVar(element, postInit)?.let { localInitVars.add(it.type) }
}
processFieldFunctionCall(node, dataclassParameters, element)
@@ -518,8 +519,8 @@ class PyDataclassInspection : PyInspection() {
dataclassParameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.DATACLASS_TRANSFORM ||
PyEvaluator.evaluateAsBoolean(PyUtil.peelArgument(dataclassParameters.others["auto_attribs"]), false)) {
cls.processClassLevelDeclarations { element, _ ->
if (element is PyTargetExpression
&& element.annotation == null
if (element is PyTargetExpression
&& element.annotation == null
&& resolveDataclassFieldParameters(cls, dataclassParameters, element, myTypeEvalContext) != null) {
registerProblem(element, PyPsiBundle.message("INSP.dataclasses.attribute.lacks.type.annotation", element.name),
ProblemHighlightType.GENERIC_ERROR)
@@ -561,20 +562,23 @@ class PyDataclassInspection : PyInspection() {
}
}
private fun processAsInitVar(field: PyTargetExpression, postInit: PyFunction?): PyTargetExpression? {
if (isInitVar(field)) {
private fun processAsInitVar(field: PyTargetExpression, postInit: PyFunction?): InitVarField? {
val fieldType = myTypeEvalContext.getType(field)
if (isInitVar(fieldType)) {
if (postInit == null) {
registerProblem(field,
PyPsiBundle.message("INSP.dataclasses.attribute.useless.until.post.init.declared", field.name),
ProblemHighlightType.LIKE_UNUSED_SYMBOL)
}
return field
return InitVarField(getInitVarType(fieldType))
}
return null
}
private class InitVarField(val type: PyType?)
private fun processFieldFunctionCall(dataclass: PyClass, dataclassParameters: PyDataclassParameters, field: PyTargetExpression) {
val fieldStub = resolveDataclassFieldParameters(dataclass, dataclassParameters, field, myTypeEvalContext) ?: return
val call = field.findAssignedValue() as? PyCallExpression ?: return
@@ -595,7 +599,7 @@ class PyDataclassInspection : PyInspection() {
private fun processPostInitDefinition(cls: PyClass,
postInit: PyFunction,
dataclassParameters: PyDataclassParameters,
localInitVars: List<PyTargetExpression>) {
localInitVars: List<PyType?>) {
if (!dataclassParameters.init) {
registerProblem(postInit.nameIdentifier,
PyPsiBundle.message("INSP.dataclasses.post.init.would.not.be.called.until.init.parameter.set.to.true"),
@@ -606,13 +610,16 @@ class PyDataclassInspection : PyInspection() {
if (ParamHelper.isSelfArgsKwargsCallable(postInit, myTypeEvalContext)) return
val allInitVars = mutableListOf<PyTargetExpression>()
val allInitVars = mutableListOf<PyType?>()
for (ancestor in cls.getAncestorClasses(myTypeEvalContext).asReversed()) {
if (parseStdDataclassParameters(ancestor, myTypeEvalContext) == null) continue
ancestor.processClassLevelDeclarations { element, _ ->
if (element is PyTargetExpression && isInitVar(element)) {
allInitVars.add(element)
if (element is PyTargetExpression) {
val fieldType = myTypeEvalContext.getType(element)
if (isInitVar(fieldType)) {
allInitVars.add(getInitVarType(fieldType))
}
}
return@processClassLevelDeclarations true
@@ -620,26 +627,31 @@ class PyDataclassInspection : PyInspection() {
}
allInitVars.addAll(localInitVars)
val implicitParameters = postInit.getParameters(myTypeEvalContext)
val parameters = if (implicitParameters.isEmpty()) emptyList<PyCallableParameter>() else ContainerUtil.subList(implicitParameters, 1)
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")
}
val parameters = postInit.getParameters(myTypeEvalContext).tailOrEmpty()
if (parameters.size != allInitVars.size) {
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")
}
registerProblem(postInit.parameterList, message, ProblemHighlightType.GENERIC_ERROR)
}
else {
parameters
.asSequence()
.zip(allInitVars.asSequence())
.all { it.first.name == it.second.name }
.also { if (!it) registerProblem(postInit.parameterList, message) }
for ((index, callableParameter) in parameters.withIndex()) {
val parameter = callableParameter.parameter
if (parameter !is PyNamedParameter) continue
val annotation = PyTypingTypeProvider.getAnnotationValue(parameter, myTypeEvalContext) ?: continue
val typeFromAnnotation = PyTypingTypeProvider.getType(annotation, myTypeEvalContext) ?: continue
val initVarType = allInitVars[index]
if (!PyTypeChecker.match(typeFromAnnotation.get(), initVarType, myTypeEvalContext)) {
val initVarTypeName = PythonDocumentationProvider.getTypeName(initVarType, myTypeEvalContext)
val parameterTypeName = PythonDocumentationProvider.getVerboseTypeName(typeFromAnnotation.get(), myTypeEvalContext)
registerProblem(annotation,
PyPsiBundle.message("INSP.dataclasses.expected.type.got.type.instead", initVarTypeName, parameterTypeName))
}
}
}
}
@@ -695,14 +707,27 @@ class PyDataclassInspection : PyInspection() {
}
private fun isInitVar(field: PyTargetExpression): Boolean {
return (myTypeEvalContext.getType(field) as? PyClassType)?.classQName == Dataclasses.DATACLASSES_INITVAR
return isInitVar(myTypeEvalContext.getType(field))
}
private fun isExpectedDataclass(type: PyType?,
dataclassType: PyDataclassParameters.PredefinedType?,
allowDefinition: Boolean,
allowInstance: Boolean,
allowSubclass: Boolean): Boolean {
private fun isInitVar(fieldType: PyType?): Boolean {
return fieldType is PyCollectionType && fieldType.classQName == Dataclasses.DATACLASSES_INITVAR
}
private fun getInitVarType(fieldType: PyType?): PyType? {
if (fieldType !is PyCollectionType || fieldType.classQName != Dataclasses.DATACLASSES_INITVAR) {
throw IllegalArgumentException()
}
return fieldType.elementTypes.singleOrNull()
}
private fun isExpectedDataclass(
type: PyType?,
dataclassType: PyDataclassParameters.PredefinedType?,
allowDefinition: Boolean,
allowInstance: Boolean,
allowSubclass: Boolean,
): Boolean {
if (type is PyStructuralType || PyTypeChecker.isUnknown(type, myTypeEvalContext)) return true
if (type is PyUnionType) return type.members.any {
isExpectedDataclass(it, dataclassType, allowDefinition, allowInstance, allowSubclass)
@@ -15,5 +15,5 @@ class B:
b: dataclasses.InitVar[str]
c: dataclasses.InitVar[bytes]
def __post_init__<warning descr="'__post_init__' should take all init-only variables in the same order as they are defined">(self, c, b)</warning>:
def __post_init__(self, c: <warning descr="Expected type 'str', got 'bytes' instead">bytes</warning>, b: bytes):
pass