PY-46661 PY-43387 TypedDict type-checking in target expressions: report value type errors, missing and extra keys

GitOrigin-RevId: 9636c8e032f9d084d7a5c3a8036e54422b8dd222
This commit is contained in:
Lada Gagina
2021-11-23 18:46:00 +03:00
committed by intellij-monorepo-bot
parent 568861178e
commit bc28fa5645
5 changed files with 114 additions and 17 deletions

View File

@@ -1083,6 +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.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

@@ -136,6 +136,12 @@ public class PyTypeCheckerInspection extends PyInspection {
if (value == null) return;
final PyType expected = myTypeEvalContext.getType(node);
final PyType actual = tryPromotingType(value, expected);
if (expected != null && actual instanceof PyTypedDictType) {
reportTypedDictProblems(expected, actual, value);
return;
}
if (!PyTypeChecker.match(expected, actual, myTypeEvalContext)) {
String expectedName = PythonDocumentationProvider.getTypeName(expected, myTypeEvalContext);
String actualName = PythonDocumentationProvider.getTypeName(actual, myTypeEvalContext);
@@ -143,6 +149,47 @@ public class PyTypeCheckerInspection extends PyInspection {
}
}
private void reportTypedDictProblems(PyType expected, PyType actual, PyExpression value) {
final Optional<PyTypedDictType.TypeCheckingResult> result =
PyTypedDictType.Companion.checkTypes(expected, (PyTypedDictType)actual,
myTypeEvalContext);
if (result.isPresent() && !result.get().getMatch()) {
final PyTypedDictType.TypeCheckingResult typeCheckingResult = result.get();
final String expectedTypedDictName = PythonDocumentationProvider.getTypeName(expected, myTypeEvalContext);
final String actualTypedDictName = PythonDocumentationProvider.getTypeName(actual, myTypeEvalContext);
if (!typeCheckingResult.getValueTypesErrors().isEmpty()) {
typeCheckingResult.getValueTypesErrors().forEach(error -> {
final PyExpression actualValueWithWrongType = error.getActualExpression();
final String expectedName = PythonDocumentationProvider.getTypeName(error.getExpectedType(), myTypeEvalContext);
final String actualName = PythonDocumentationProvider.getTypeName(error.getActualType(), myTypeEvalContext);
registerProblem(Objects.requireNonNullElse(actualValueWithWrongType, value),
PyPsiBundle.message("INSP.type.checker.expected.type.got.type.instead",
actualValueWithWrongType != null ? expectedName : expectedTypedDictName,
actualValueWithWrongType != null ? actualName : actualTypedDictName));
});
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)))));
}
if (!typeCheckingResult.getMissingKeys().isEmpty()) {
registerProblem(value,
PyPsiBundle.message("INSP.type.checker.typed.dict.missing.key", expectedTypedDictName,
typeCheckingResult.getMissingKeys().size(),
String.join(", ",
ContainerUtil.map(typeCheckingResult.getMissingKeys(),
s -> String.format("'%s'", s)))));
}
}
}
@Nullable
private PyType tryPromotingType(@NotNull PyExpression value, @Nullable PyType expected) {
return tryPromotingType(value, expected, myTypeEvalContext);

View File

@@ -312,8 +312,8 @@ public final class PyTypeChecker {
}
if (actual instanceof PyTypedDictType) {
final Optional<Boolean> match = PyTypedDictType.Companion.match(expected, (PyTypedDictType)actual, context);
if (match.isPresent()) return match;
final Optional<PyTypedDictType.TypeCheckingResult> matchResult = PyTypedDictType.Companion.checkTypes(expected, (PyTypedDictType)actual, context);
if (matchResult.isPresent()) return Optional.of(matchResult.get().getMatch());
}
final PyClass superClass = expected.getPyClass();

View File

@@ -159,7 +159,7 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
* * all keys from [actual] are present in [expected]
* * each key has the same value type in [expected] and [actual]
*/
fun match(expected: PyType, actual: PyTypedDictType, context: TypeEvalContext): Optional<Boolean> {
fun checkTypes(expected: PyType, actual: PyTypedDictType, context: TypeEvalContext): Optional<TypeCheckingResult> {
if (!actual.isInferred()) {
val match = checkStructuralCompatibility(expected, actual, context)
if (match.isPresent) {
@@ -178,12 +178,21 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
private fun match(mandatoryArguments: Map<String, Pair<PyExpression?, PyType?>>,
expectedArguments: Map<String, Pair<PyExpression?, PyType?>>,
actualArguments: Map<String, Pair<PyExpression?, PyType?>>,
context: TypeEvalContext): Boolean {
if (!actualArguments.keys.containsAll(mandatoryArguments.keys)) return false
context: TypeEvalContext): TypeCheckingResult {
val valueTypesErrors = mutableListOf<ValueTypeError>()
val keysMissing = mutableListOf<String>()
val extraKeys = mutableListOf<String>()
var match = true
if (!actualArguments.keys.containsAll(mandatoryArguments.keys)) {
keysMissing.addAll(mandatoryArguments.keys.filter { !actualArguments.containsKey(it) })
match = false
}
actualArguments.forEach {
if (!expectedArguments.containsKey(it.key)) {
return false
extraKeys.add(it.key)
match = false
}
val actualValue = it.value.first
val expectedType = expectedArguments[it.key]?.second
@@ -194,11 +203,12 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
context
)
if (!matchResult) {
return false
valueTypesErrors.add(ValueTypeError(it.value.first, expectedType, it.value.second))
match = false
}
}
return true
return TypeCheckingResult(match, valueTypesErrors, keysMissing, extraKeys)
}
private fun strictUnionMatch(expected: PyType?, actual: PyType?, context: TypeEvalContext): Boolean {
@@ -209,31 +219,50 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String,
* Rules for type-checking TypedDicts are described in PEP-589
* @see <a href=https://www.python.org/dev/peps/pep-0589/#type-consistency>PEP-589</a>
*/
fun checkStructuralCompatibility(expected: PyType?, actual: PyTypedDictType, context: TypeEvalContext): Optional<Boolean> {
fun checkStructuralCompatibility(expected: PyType?,
actual: PyTypedDictType,
context: TypeEvalContext): Optional<TypeCheckingResult> {
if (expected is PyCollectionType && PyTypingTypeProvider.MAPPING == expected.classQName) {
val builtinCache = PyBuiltinCache.getInstance(actual.dictClass)
val elementTypes = expected.elementTypes
return Optional.of(elementTypes.size == 2
&& builtinCache.strType == elementTypes[0]
&& (elementTypes[1] == null || PyNames.OBJECT == elementTypes[1].name))
return Optional.of(TypeCheckingResult(elementTypes.size == 2
&& builtinCache.strType == elementTypes[0]
&& (elementTypes[1] == null || PyNames.OBJECT == elementTypes[1].name), emptyList(), emptyList(), emptyList()))
}
if (expected !is PyTypedDictType) return Optional.empty()
expected.fields.forEach {
val expectedTypeAndTotality = it.value
val valueTypesErrors = mutableListOf<ValueTypeError>()
val keysMissing = mutableListOf<String>()
var match = true
if (!actual.fields.containsKey(it.key)) return Optional.of(false)
expected.fields.forEach {
if (!actual.fields.containsKey(it.key)) {
keysMissing.add(it.key)
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)) {
return Optional.of(false)
valueTypesErrors.add(ValueTypeError(null, expectedTypeAndTotality.type, actualTypeAndTotality?.type))
match = false
}
}
return Optional.of(true)
return Optional.of(TypeCheckingResult(match, valueTypesErrors, keysMissing, emptyList()))
}
}
class ValueTypeError constructor(val actualExpression: PyExpression?,
val expectedType: PyType?,
val actualType: PyType?)
class TypeCheckingResult constructor(val match: Boolean,
val valueTypesErrors: List<ValueTypeError>,
val missingKeys: List<String>,
val extraKeys: List<String>)
}