mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-18 04:21:24 +07:00
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:
committed by
intellij-monorepo-bot
parent
568861178e
commit
bc28fa5645
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user