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:
Lada Gagina
2021-11-23 21:52:07 +03:00
committed by intellij-monorepo-bot
parent bc28fa5645
commit 72772c55da
4 changed files with 57 additions and 28 deletions

View File

@@ -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}

View File

@@ -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);
}

View File

@@ -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>)
}

View File

@@ -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>)"));
}
}