PY-75655 False negative when updating ReadOnly TypedDict field using update() method

GitOrigin-RevId: 42428a0541893481046952407fce2d99a02081fa
This commit is contained in:
Andrey Vokin
2024-09-04 08:38:23 +02:00
committed by intellij-monorepo-bot
parent 6a08ff233d
commit 08ad3f7789
2 changed files with 34 additions and 5 deletions

View File

@@ -191,12 +191,20 @@ class PyTypedDictInspection : PyInspection() {
val nodeType = myTypeEvalContext.getType(callee.qualifier!!)
if (nodeType !is PyTypedDictType || nodeType.isInferred()) return
val arguments = node.arguments
var arguments = node.arguments
if (PyNames.UPDATE == callee.name) {
inspectUpdateSequenceArgument(
if (arguments.size == 1 && arguments[0] is PySequenceExpression) (arguments[0] as PySequenceExpression).elements else arguments,
nodeType)
if (arguments.size == 1 && arguments[0] is PyReferenceExpression) {
(PyUtil.resolveToTheTop(arguments[0]) as? PyTargetExpression)?.let { resolvedArg ->
resolvedArg.findAssignedValue()?.let {
arguments = arrayOf<PyExpression>(it)
}
}
}
if (arguments.size == 1 && arguments[0] is PySequenceExpression) {
arguments = (arguments[0] as PySequenceExpression).elements
}
inspectUpdateSequenceArgument(node, arguments, nodeType)
}
if (PyNames.CLEAR == callee.name || PyNames.POPITEM == callee.name) {
@@ -390,7 +398,7 @@ class PyTypedDictInspection : PyInspection() {
PyTypeChecker.match(expected.type, actual.type, myTypeEvalContext)
}
private fun inspectUpdateSequenceArgument(sequenceElements: Array<PyExpression>, typedDictType: PyTypedDictType) {
private fun inspectUpdateSequenceArgument(updateCall: PyCallExpression, sequenceElements: Array<PyExpression>, typedDictType: PyTypedDictType) {
sequenceElements.forEach {
var key: PsiElement? = null
var keyAsString: String? = null
@@ -430,6 +438,10 @@ class PyTypedDictInspection : PyInspection() {
registerProblem(key, PyPsiBundle.message("INSP.typeddict.typeddict.cannot.have.key", typedDictType.name, keyAsString))
return@forEach
}
if (fields.get(keyAsString)!!.qualifiers.isReadOnly) {
val warningHolder = (updateCall.callee as? PyReferenceExpression)?.nameElement?.psi ?: updateCall
registerProblem(warningHolder, PyPsiBundle.message("INSP.typeddict.typeddict.field.is.readonly", keyAsString))
}
val valueType = myTypeEvalContext.getType(value)
if (!PyTypeChecker.match(fields[keyAsString]?.type, valueType, myTypeEvalContext)) {
val expectedTypeName = PythonDocumentationProvider.getTypeName(fields[keyAsString]!!.type, myTypeEvalContext)

View File

@@ -451,6 +451,23 @@ public class PyTypedDictInspectionTest extends PyInspectionTestCase {
);
}
public void testUpdateReadOnlyMember() {
doTestByText(
"""
from typing import TypedDict, NotRequired
from typing_extensions import ReadOnly
class A(TypedDict):
x: NotRequired[ReadOnly[int]]
y: int
a1: A = { "x": 1, "y": 1 }
a2: A = { "x": 2, "y": 4 }
a1.<warning descr="TypedDict key \\"x\\" is ReadOnly">update</warning>(a2)
"""
);
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {