mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 21:11:28 +07:00
PY-46661 PY-43387 TypedDict type-checking in call expressions: report value type errors, missing and extra keys
GitOrigin-RevId: ad8c2e37cf893e50aeda7a54d113dec571b0d135
This commit is contained in:
committed by
intellij-monorepo-bot
parent
bc28fa5645
commit
72772c55da
@@ -1083,8 +1083,8 @@ QFIX.ignore.shadowed.built.in.name=Ignore shadowed built-in name "{0}"
|
||||
# PyTypeCheckerInspection
|
||||
INSP.NAME.type.checker=Incorrect type
|
||||
INSP.type.checker.expected.type.got.type.instead=Expected type ''{0}'', got ''{1}'' instead
|
||||
INSP.type.checker.typed.dict.extra.key=TypedDict ''{0}'' has extra {1,choice,1#key|2#keys}: {2}
|
||||
INSP.type.checker.typed.dict.missing.key=TypedDict ''{0}'' has missing {1,choice,1#key|2#keys}: {2}
|
||||
INSP.type.checker.typed.dict.extra.key=Extra key ''{0}'' for TypedDict ''{1}''
|
||||
INSP.type.checker.typed.dict.missing.keys=TypedDict ''{0}'' has missing {1,choice,1#key|2#keys}: {2}
|
||||
INSP.type.checker.expected.to.return.type.got.no.return=Expected to return ''{0}'', got no return
|
||||
INSP.type.checker.init.should.return.none=__init__ should return None
|
||||
INSP.type.checker.type.does.not.have.expected.attribute=Type ''{0}'' doesn''t have expected {1,choice,1#attribute|2#attributes} {2}
|
||||
|
||||
@@ -172,16 +172,15 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
if (!((PyTypedDictType)actual).isInferred()) return;
|
||||
}
|
||||
if (!typeCheckingResult.getExtraKeys().isEmpty()) {
|
||||
registerProblem(value,
|
||||
PyPsiBundle.message("INSP.type.checker.typed.dict.extra.key", expectedTypedDictName,
|
||||
typeCheckingResult.getExtraKeys().size(),
|
||||
String.join(", ",
|
||||
ContainerUtil.map(typeCheckingResult.getExtraKeys(),
|
||||
s -> String.format("'%s'", s)))));
|
||||
typeCheckingResult.getExtraKeys().forEach(error -> {
|
||||
final PyExpression actualValueWithWrongType = error.getActualExpression();
|
||||
registerProblem(Objects.requireNonNullElse(actualValueWithWrongType, value),
|
||||
PyPsiBundle.message("INSP.type.checker.typed.dict.extra.key", error.getKey(), expectedTypedDictName));
|
||||
});
|
||||
}
|
||||
if (!typeCheckingResult.getMissingKeys().isEmpty()) {
|
||||
registerProblem(value,
|
||||
PyPsiBundle.message("INSP.type.checker.typed.dict.missing.key", expectedTypedDictName,
|
||||
PyPsiBundle.message("INSP.type.checker.typed.dict.missing.keys", expectedTypedDictName,
|
||||
typeCheckingResult.getMissingKeys().size(),
|
||||
String.join(", ",
|
||||
ContainerUtil.map(typeCheckingResult.getMissingKeys(),
|
||||
@@ -326,7 +325,7 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
final boolean matched = matchParameterAndArgument(expected, actual, substitutions);
|
||||
final boolean matched = matchParameterAndArgument(expected, actual, argument, substitutions);
|
||||
result.add(new AnalyzeArgumentResult(argument, expected, substituteGenerics(expected, substitutions), actual, matched));
|
||||
}
|
||||
}
|
||||
@@ -361,7 +360,7 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
final var expected = types.get(i);
|
||||
final var argument = arguments.get(i);
|
||||
final var actual = myTypeEvalContext.getType(argument);
|
||||
final var matched = matchParameterAndArgument(expected, actual, substitutions);
|
||||
final var matched = matchParameterAndArgument(expected, actual, argument, substitutions);
|
||||
result.add(new AnalyzeArgumentResult(argument, expected, substituteGenerics(expected, substitutions), actual, matched));
|
||||
}
|
||||
}
|
||||
@@ -375,7 +374,7 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
// For an expected type with generics we have to match all the actual types against it in order to do proper generic unification
|
||||
if (PyTypeChecker.hasGenerics(expected, myTypeEvalContext)) {
|
||||
final PyType actual = PyUnionType.union(ContainerUtil.map(arguments, myTypeEvalContext::getType));
|
||||
final boolean matched = matchParameterAndArgument(expected, actual, substitutions);
|
||||
final boolean matched = matchParameterAndArgument(expected, actual, null, substitutions);
|
||||
return ContainerUtil.map(arguments,
|
||||
argument -> new AnalyzeArgumentResult(argument, expected, expectedWithSubstitutions, actual, matched));
|
||||
}
|
||||
@@ -384,7 +383,7 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
arguments,
|
||||
argument -> {
|
||||
final PyType actual = myTypeEvalContext.getType(argument);
|
||||
final boolean matched = matchParameterAndArgument(expected, actual, substitutions);
|
||||
final boolean matched = matchParameterAndArgument(expected, actual, argument, substitutions);
|
||||
return new AnalyzeArgumentResult(argument, expected, expectedWithSubstitutions, actual, matched);
|
||||
}
|
||||
);
|
||||
@@ -393,7 +392,13 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
|
||||
private boolean matchParameterAndArgument(@Nullable PyType parameterType,
|
||||
@Nullable PyType argumentType,
|
||||
@Nullable PyExpression argument,
|
||||
@NotNull PyTypeChecker.GenericSubstitutions substitutions) {
|
||||
if (parameterType != null && argumentType instanceof PyTypedDictType) {
|
||||
reportTypedDictProblems(parameterType, argumentType, argument);
|
||||
return true;
|
||||
}
|
||||
|
||||
return PyTypeChecker.match(parameterType, argumentType, myTypeEvalContext, substitutions) &&
|
||||
!PyProtocolsKt.matchingProtocolDefinitions(parameterType, argumentType, myTypeEvalContext);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package com.jetbrains.python.psi.types
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.jetbrains.python.PyNames
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
|
||||
import com.jetbrains.python.codeInsight.typing.TDFields
|
||||
@@ -181,7 +182,7 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
|
||||
context: TypeEvalContext): TypeCheckingResult {
|
||||
val valueTypesErrors = mutableListOf<ValueTypeError>()
|
||||
val keysMissing = mutableListOf<String>()
|
||||
val extraKeys = mutableListOf<String>()
|
||||
val extraKeys = mutableListOf<ExtraKeyError>()
|
||||
var match = true
|
||||
|
||||
if (!actualArguments.keys.containsAll(mandatoryArguments.keys)) {
|
||||
@@ -191,7 +192,8 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
|
||||
|
||||
actualArguments.forEach {
|
||||
if (!expectedArguments.containsKey(it.key)) {
|
||||
extraKeys.add(it.key)
|
||||
val pairArgument = PsiTreeUtil.getParentOfType(it.value.first, PyKeyValueExpression::class.java)
|
||||
extraKeys.add(ExtraKeyError(pairArgument ?: it.value.first, it.key))
|
||||
match = false
|
||||
}
|
||||
val actualValue = it.value.first
|
||||
@@ -227,7 +229,8 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
|
||||
val elementTypes = expected.elementTypes
|
||||
return Optional.of(TypeCheckingResult(elementTypes.size == 2
|
||||
&& builtinCache.strType == elementTypes[0]
|
||||
&& (elementTypes[1] == null || PyNames.OBJECT == elementTypes[1].name), emptyList(), emptyList(), emptyList()))
|
||||
&& (elementTypes[1] == null || PyNames.OBJECT == elementTypes[1].name), emptyList(),
|
||||
emptyList(), emptyList()))
|
||||
}
|
||||
|
||||
if (expected !is PyTypedDictType) return Optional.empty()
|
||||
@@ -237,19 +240,19 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
|
||||
var match = true
|
||||
|
||||
expected.fields.forEach {
|
||||
if (!actual.fields.containsKey(it.key)) {
|
||||
keysMissing.add(it.key)
|
||||
val expectedTypeAndTotality = it.value
|
||||
val actualTypeAndTotality = actual.fields[it.key]
|
||||
|
||||
if (match && (actualTypeAndTotality == null
|
||||
|| !strictUnionMatch(expectedTypeAndTotality.type, actualTypeAndTotality.type, context)
|
||||
|| !strictUnionMatch(actualTypeAndTotality.type, expectedTypeAndTotality.type, context)
|
||||
|| expectedTypeAndTotality.isRequired.xor(actualTypeAndTotality.isRequired))) {
|
||||
valueTypesErrors.add(ValueTypeError(null, expectedTypeAndTotality.type, actualTypeAndTotality?.type))
|
||||
match = false
|
||||
}
|
||||
|
||||
val expectedTypeAndTotality = it.value
|
||||
|
||||
val actualTypeAndTotality = actual.fields[it.key]
|
||||
if (actualTypeAndTotality == null
|
||||
|| !strictUnionMatch(expectedTypeAndTotality.type, actualTypeAndTotality.type, context)
|
||||
|| !strictUnionMatch(actualTypeAndTotality.type, expectedTypeAndTotality.type, context)
|
||||
|| expectedTypeAndTotality.isRequired.xor(actualTypeAndTotality.isRequired)) {
|
||||
valueTypesErrors.add(ValueTypeError(null, expectedTypeAndTotality.type, actualTypeAndTotality?.type))
|
||||
if (!actual.fields.containsKey(it.key)) {
|
||||
keysMissing.add(it.key)
|
||||
match = false
|
||||
}
|
||||
}
|
||||
@@ -257,6 +260,9 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraKeyError constructor(val actualExpression: PyExpression?,
|
||||
val key: String)
|
||||
|
||||
class ValueTypeError constructor(val actualExpression: PyExpression?,
|
||||
val expectedType: PyType?,
|
||||
val actualType: PyType?)
|
||||
@@ -264,5 +270,5 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
|
||||
class TypeCheckingResult constructor(val match: Boolean,
|
||||
val valueTypesErrors: List<ValueTypeError>,
|
||||
val missingKeys: List<String>,
|
||||
val extraKeys: List<String>)
|
||||
val extraKeys: List<ExtraKeyError>)
|
||||
}
|
||||
|
||||
@@ -1386,7 +1386,25 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase {
|
||||
"p2: NotPoint = {'x': <warning descr=\"Expected type 'int', got 'str' instead\">'x'</warning>, 'y': <warning descr=\"Expected type 'str', got 'int' instead\">42</warning>}\n" +
|
||||
"p3: Point = <warning descr=\"Expected type 'Point', got 'NotPoint' instead\">p2</warning>\n" +
|
||||
"p4: Point = <warning descr=\"TypedDict 'Point' has missing keys: 'x', 'y'\">{}</warning>\n" +
|
||||
"p5: Point = <warning descr=\"TypedDict 'Point' has extra keys: 'z', 'k', 'n'\">{'x': 0, 'y': 0, 'z': 123, 'k': 6, 'n': ''}</warning>\n" +
|
||||
"p5: Point = {'x': 0, 'y': 0, <warning descr=\"Extra key 'z' for TypedDict 'Point'\">'z': 123</warning>, <warning descr=\"Extra key 'k' for TypedDict 'Point'\">'k': 6</warning>}\n" +
|
||||
"p6: Point = <warning descr=\"TypedDict 'Point' has missing key: 'x'\">{'y': 123}</warning>"));
|
||||
}
|
||||
|
||||
// PY-46661
|
||||
public void testCustomErrorMessagesForTypedDictInCallExpressions() {
|
||||
runWithLanguageLevel(
|
||||
LanguageLevel.getLatest(),
|
||||
() -> doTestByText("from typing import TypedDict\n" +
|
||||
"class Point(TypedDict):\n" +
|
||||
" x: int\n" +
|
||||
" y: int\n" +
|
||||
"class Movie(TypedDict):\n" +
|
||||
" name: str\n" +
|
||||
" year: int\n" +
|
||||
"def record_movie(movie: Movie) -> None: ...\n" +
|
||||
"record_movie({'name': <warning descr=\"Expected type 'str', got 'int' instead\">1984</warning>, 'year': 1984})\n" +
|
||||
"record_movie(<warning descr=\"TypedDict 'Movie' has missing keys: 'name', 'year'\">{}</warning>)\n" +
|
||||
"record_movie({'name': '1984', 'year': 1984, <warning descr=\"Extra key 'director' for TypedDict 'Movie'\">'director': 'Michael Radford'</warning>})\n" +
|
||||
"record_movie(<warning descr=\"Expected type 'Movie', got 'Point' instead\">Point(x=123, y=321)</warning>)"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user