[ty] Infer types for key-based access on TypedDicts (#19763)

## Summary

This PR adds type inference for key-based access on `TypedDict`s and a
new diagnostic for invalid subscript accesses:

```py
class Person(TypedDict):
    name: str
    age: int | None

alice = Person(name="Alice", age=25)

reveal_type(alice["name"])  # revealed: str
reveal_type(alice["age"])  # revealed: int | None

alice["naem"]  # Unknown key "naem" - did you mean "name"?
```

## Test Plan

Updated Markdown tests
This commit is contained in:
David Peter 2025-08-06 09:36:33 +02:00 committed by GitHub
parent e917d309f1
commit 4887bdf205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 489 additions and 158 deletions

View file

@ -46,6 +46,7 @@ mod util;
#[cfg(feature = "testing")]
pub mod pull_types;
type FxOrderMap<K, V> = ordermap::map::OrderMap<K, V, BuildHasherDefault<FxHasher>>;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
type FxIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<FxHasher>>;
type FxIndexSet<V> = indexmap::IndexSet<V, BuildHasherDefault<FxHasher>>;

View file

@ -38,6 +38,7 @@ use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::{imported_modules, place_table, semantic_index};
use crate::suppression::check_suppressions;
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
use crate::types::class::{CodeGeneratorKind, Field};
pub(crate) use crate::types::class_base::ClassBase;
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
@ -61,7 +62,7 @@ use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signat
use crate::types::tuple::{TupleSpec, TupleType};
use crate::unpack::EvaluationMode;
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program};
use crate::{Db, FxOrderMap, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
use instance::Protocol;
pub use instance::{NominalInstanceType, ProtocolInstanceType};
@ -669,10 +670,6 @@ impl<'db> Type<'db> {
matches!(self, Type::Dynamic(_))
}
pub(crate) const fn is_typed_dict(&self) -> bool {
matches!(self, Type::TypedDict(..))
}
/// Returns the top materialization (or upper bound materialization) of this type, which is the
/// most general form of the type that is fully static.
#[must_use]
@ -834,6 +831,17 @@ impl<'db> Type<'db> {
.expect("Expected a Type::EnumLiteral variant")
}
pub(crate) const fn is_typed_dict(&self) -> bool {
matches!(self, Type::TypedDict(..))
}
pub(crate) fn into_typed_dict(self) -> Option<TypedDictType<'db>> {
match self {
Type::TypedDict(typed_dict) => Some(typed_dict),
_ => None,
}
}
/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
/// Since a `ClassType` must be specialized, apply the default specialization to any
/// unspecialized generic class literal.
@ -5289,15 +5297,15 @@ impl<'db> Type<'db> {
],
),
_ if class.is_typed_dict(db) => {
Type::TypedDict(TypedDictType::new(db, ClassType::NonGeneric(*class)))
TypedDictType::from(db, ClassType::NonGeneric(*class))
}
_ => Type::instance(db, class.default_specialization(db)),
};
Ok(ty)
}
Type::GenericAlias(alias) if alias.is_typed_dict(db) => Ok(Type::TypedDict(
TypedDictType::new(db, ClassType::from(*alias)),
)),
Type::GenericAlias(alias) if alias.is_typed_dict(db) => {
Ok(TypedDictType::from(db, ClassType::from(*alias)))
}
Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))),
Type::SubclassOf(_)
@ -5644,6 +5652,7 @@ impl<'db> Type<'db> {
return KnownClass::Dict
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object(db)])
.map(Type::from)
// Guard against user-customized typesheds with a broken `dict` class
.unwrap_or_else(Type::unknown);
}
@ -9000,6 +9009,15 @@ pub struct TypedDictType<'db> {
impl get_size2::GetSize for TypedDictType<'_> {}
impl<'db> TypedDictType<'db> {
pub(crate) fn from(db: &'db dyn Db, defining_class: ClassType<'db>) -> Type<'db> {
Type::TypedDict(Self::new(db, defining_class))
}
pub(crate) fn items(self, db: &'db dyn Db) -> FxOrderMap<Name, Field<'db>> {
let (class_literal, specialization) = self.defining_class(db).class_literal(db);
class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict)
}
pub(crate) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,

View file

@ -1,4 +1,3 @@
use std::hash::BuildHasherDefault;
use std::sync::{LazyLock, Mutex};
use super::TypeVarVariance;
@ -9,6 +8,7 @@ use super::{
function::{FunctionDecorators, FunctionType},
infer_expression_type, infer_unpack_types,
};
use crate::FxOrderMap;
use crate::module_resolver::KnownModule;
use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::semantic_index::scope::NodeWithScopeKind;
@ -23,9 +23,9 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu
use crate::types::tuple::TupleSpec;
use crate::types::{
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
DeprecatedInstance, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
infer_definition_types, todo_type,
DeprecatedInstance, KnownInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
declaration_type, infer_definition_types, todo_type,
};
use crate::{
Db, FxIndexMap, FxOrderSet, Program,
@ -54,9 +54,7 @@ use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, PythonVersion};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::{FxHashSet, FxHasher};
type FxOrderMap<K, V> = ordermap::map::OrderMap<K, V, BuildHasherDefault<FxHasher>>;
use rustc_hash::FxHashSet;
fn explicit_bases_cycle_recover<'db>(
_db: &'db dyn Db,
@ -1097,11 +1095,11 @@ impl MethodDecorator {
}
}
/// Metadata regarding a dataclass field/attribute.
/// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DataclassField<'db> {
pub(crate) struct Field<'db> {
/// The declared type of the field
pub(crate) field_ty: Type<'db>,
pub(crate) declared_ty: Type<'db>,
/// The type of the default value for this field
pub(crate) default_ty: Option<Type<'db>>,
@ -1858,8 +1856,8 @@ impl<'db> ClassLiteral<'db> {
let mut kw_only_field_seen = false;
for (
field_name,
DataclassField {
mut field_ty,
Field {
declared_ty: mut field_ty,
mut default_ty,
init_only: _,
init,
@ -2038,17 +2036,28 @@ impl<'db> ClassLiteral<'db> {
Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::TypedDict, "__getitem__") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
]),
Some(todo_type!("Support for `TypedDict`")),
);
let fields = self.fields(db, specialization, field_policy);
Some(CallableType::function_like(db, signature))
// Add (key -> value type) overloads for all TypedDict items ("fields"):
let overloads = fields.iter().map(|(name, field)| {
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(key_type),
]),
Some(field.declared_ty),
)
});
Some(Type::Callable(CallableType::new(
db,
CallableSignature::from_overloads(overloads),
true,
)))
}
(CodeGeneratorKind::TypedDict, "get") => {
// TODO: synthesize a set of overloads with precise types
@ -2143,7 +2152,7 @@ impl<'db> ClassLiteral<'db> {
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
field_policy: CodeGeneratorKind,
) -> FxOrderMap<Name, DataclassField<'db>> {
) -> FxOrderMap<Name, Field<'db>> {
if field_policy == CodeGeneratorKind::NamedTuple {
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
// fields of this class only.
@ -2190,7 +2199,7 @@ impl<'db> ClassLiteral<'db> {
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
) -> FxOrderMap<Name, DataclassField<'db>> {
) -> FxOrderMap<Name, Field<'db>> {
let mut attributes = FxOrderMap::default();
let class_body_scope = self.body_scope(db);
@ -2242,8 +2251,8 @@ impl<'db> ClassLiteral<'db> {
attributes.insert(
symbol.name().clone(),
DataclassField {
field_ty: attr_ty.apply_optional_specialization(db, specialization),
Field {
declared_ty: attr_ty.apply_optional_specialization(db, specialization),
default_ty,
init_only: attr.is_init_var(),
init,

View file

@ -40,6 +40,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INSTANCE_LAYOUT_CONFLICT);
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
registry.register_lint(&INVALID_KEY);
registry.register_lint(&INVALID_ARGUMENT_TYPE);
registry.register_lint(&INVALID_RETURN_TYPE);
registry.register_lint(&INVALID_ASSIGNMENT);
@ -489,6 +490,31 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for subscript accesses with invalid keys.
///
/// ## Why is this bad?
/// Using an invalid key will raise a `KeyError` at runtime.
///
/// ## Examples
/// ```python
/// from typing import TypedDict
///
/// class Person(TypedDict):
/// name: str
/// age: int
///
/// alice = Person(name="Alice", age=30)
/// alice["height"] # KeyError: 'height'
/// ```
pub(crate) static INVALID_KEY = {
summary: "detects invalid subscript accesses",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Detects call arguments whose type is not assignable to the corresponding typed parameter.
@ -2591,3 +2617,29 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving modules");
}
/// Suggest a name from `existing_names` that is similar to `wrong_name`.
pub(super) fn did_you_mean<S: AsRef<str>, T: AsRef<str>>(
existing_names: impl Iterator<Item = S>,
wrong_name: T,
) -> Option<String> {
if wrong_name.as_ref().len() < 3 {
return None;
}
existing_names
.filter(|ref id| id.as_ref().len() >= 2)
.map(|ref id| {
(
id.as_ref().to_string(),
strsim::damerau_levenshtein(
&id.as_ref().to_lowercase(),
&wrong_name.as_ref().to_lowercase(),
),
)
})
.min_by_key(|(_, dist)| *dist)
// Heuristic to filter out bad matches
.filter(|(_, dist)| *dist <= 3)
.map(|(id, _)| id)
}

View file

@ -90,21 +90,21 @@ use crate::semantic_index::{
ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, semantic_index,
};
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
use crate::types::class::{CodeGeneratorKind, DataclassField, MetaclassErrorKind, SliceLiteral};
use crate::types::class::{CodeGeneratorKind, Field, MetaclassErrorKind, SliceLiteral};
use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_return_type,
report_possibly_unbound_attribute,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_PARAMETER_DEFAULT,
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, did_you_mean,
report_implicit_return_type, report_instance_layout_conflict,
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_return_type, report_possibly_unbound_attribute,
};
use crate::types::enums::is_enum_class;
use crate::types::function::{
@ -1352,8 +1352,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let specialization = None;
let mut kw_only_field_names = vec![];
for (name, DataclassField { field_ty, .. }) in
class.fields(self.db(), specialization, field_policy)
for (
name,
Field {
declared_ty: field_ty,
..
},
) in class.fields(self.db(), specialization, field_policy)
{
let Some(instance) = field_ty.into_nominal_instance() else {
continue;
@ -1909,17 +1914,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: also consider qualifiers on the attribute
return (ty, is_modifiable);
}
} else if let AnyNodeRef::ExprSubscript(ast::ExprSubscript {
value,
slice,
ctx,
..
}) = node
} else if let AnyNodeRef::ExprSubscript(
subscript @ ast::ExprSubscript {
value, slice, ctx, ..
},
) = node
{
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);
let result_ty = self
.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
let result_ty = self.infer_subscript_expression_types(
subscript, value_ty, slice_ty, *ctx,
);
return (result_ty, is_modifiable);
}
}
@ -2030,24 +2035,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// pyright. TODO: Other standard library classes may also be considered safe. Also,
// subclasses of these safe classes that do not override `__getitem__/__setitem__`
// may be considered safe.
let safe_mutable_classes = [
KnownClass::List.to_instance(db),
KnownClass::Dict.to_instance(db),
KnownClass::Bytearray.to_instance(db),
KnownClass::DefaultDict.to_instance(db),
SpecialFormType::ChainMap.instance_fallback(db),
SpecialFormType::Counter.instance_fallback(db),
SpecialFormType::Deque.instance_fallback(db),
SpecialFormType::OrderedDict.instance_fallback(db),
SpecialFormType::TypedDict.instance_fallback(db),
];
if safe_mutable_classes.iter().all(|safe_mutable_class| {
!value_ty.is_equivalent_to(db, *safe_mutable_class)
&& value_ty
.generic_origin(db)
.zip(safe_mutable_class.generic_origin(db))
.is_none_or(|(l, r)| l != r)
}) {
let is_safe_mutable_class = || {
let safe_mutable_classes = [
KnownClass::List.to_instance(db),
KnownClass::Dict.to_instance(db),
KnownClass::Bytearray.to_instance(db),
KnownClass::DefaultDict.to_instance(db),
SpecialFormType::ChainMap.instance_fallback(db),
SpecialFormType::Counter.instance_fallback(db),
SpecialFormType::Deque.instance_fallback(db),
SpecialFormType::OrderedDict.instance_fallback(db),
SpecialFormType::TypedDict.instance_fallback(db),
];
safe_mutable_classes.iter().any(|safe_mutable_class| {
value_ty.is_equivalent_to(db, *safe_mutable_class)
|| value_ty
.generic_origin(db)
.zip(safe_mutable_class.generic_origin(db))
.is_some_and(|(l, r)| l == r)
})
};
if !value_ty.is_typed_dict() && !is_safe_mutable_class() {
bound_ty = declared_ty;
}
}
@ -8454,7 +8464,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ExprContext::Store => {
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);
self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
Type::Never
}
ExprContext::Del => {
@ -8464,7 +8474,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ExprContext::Invalid => {
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);
self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
Type::unknown()
}
}
@ -8493,7 +8503,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Even if we can obtain the subscript type based on the assignments, we still perform default type inference
// (to store the expression type and to report errors).
let slice_ty = self.infer_expression(slice);
self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
return ty;
}
}
@ -8532,7 +8542,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
let slice_ty = self.infer_expression(slice);
let result_ty = self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys)
}
@ -8583,7 +8593,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_subscript_expression_types(
&self,
value_node: &'ast ast::Expr,
subscript: &ast::ExprSubscript,
value_ty: Type<'db>,
slice_ty: Type<'db>,
expr_context: ExprContext,
@ -8591,12 +8601,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let db = self.db();
let context = &self.context;
let value_node = subscript.value.as_ref();
let inferred = match (value_ty, slice_ty) {
(Type::NominalInstance(instance), _)
if instance.class.is_known(db, KnownClass::VersionInfo) =>
{
Some(self.infer_subscript_expression_types(
value_node,
subscript,
Type::version_info_tuple(db),
slice_ty,
expr_context,
@ -8604,7 +8616,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
(Type::Union(union), _) => Some(union.map(db, |element| {
self.infer_subscript_expression_types(value_node, *element, slice_ty, expr_context)
self.infer_subscript_expression_types(subscript, *element, slice_ty, expr_context)
})),
// TODO: we can map over the intersection and fold the results back into an intersection,
@ -8735,7 +8747,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_),
Type::BooleanLiteral(bool),
) => Some(self.infer_subscript_expression_types(
value_node,
subscript,
value_ty,
Type::IntLiteral(i64::from(bool)),
expr_context,
@ -8830,7 +8842,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
//
// See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem
match value_ty.try_call_dunder(db, "__getitem__", CallArguments::positional([slice_ty])) {
Ok(outcome) => return outcome.return_type(db),
Ok(outcome) => {
return outcome.return_type(db);
}
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
if let Some(builder) =
context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, value_node)
@ -8856,16 +8870,61 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
CallErrorKind::BindingError => {
if let Some(builder) =
context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
{
builder.into_diagnostic(format_args!(
"Method `__getitem__` of type `{}` cannot be called with key of \
type `{}` on object of type `{}`",
bindings.callable_type().display(db),
slice_ty.display(db),
value_ty.display(db),
));
if let Some(typed_dict) = value_ty.into_typed_dict() {
let slice_node = subscript.slice.as_ref();
if let Some(builder) = context.report_lint(&INVALID_KEY, slice_node) {
match slice_ty {
Type::StringLiteral(key) => {
let key = key.value(db);
let typed_dict_name = value_ty.display(db);
let mut diagnostic = builder.into_diagnostic(format_args!(
"Invalid key access on TypedDict `{typed_dict_name}`",
));
diagnostic.annotate(
self.context.secondary(value_node).message(
format_args!("TypedDict `{typed_dict_name}`"),
),
);
let items = typed_dict.items(db);
let existing_keys =
items.iter().map(|(name, _)| name.as_str());
diagnostic.set_primary_message(format!(
"Unknown key \"{key}\"{hint}",
hint = if let Some(suggestion) =
did_you_mean(existing_keys, key)
{
format!(" - did you mean \"{suggestion}\"?")
} else {
String::new()
}
));
diagnostic
}
_ => builder.into_diagnostic(format_args!(
"TypedDict `{}` cannot be indexed with a key of type `{}`",
value_ty.display(db),
slice_ty.display(db),
)),
};
}
} else {
if let Some(builder) =
context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
{
builder.into_diagnostic(format_args!(
"Method `__getitem__` of type `{}` cannot be called with key of \
type `{}` on object of type `{}`",
bindings.callable_type().display(db),
slice_ty.display(db),
value_ty.display(db),
));
}
}
}
CallErrorKind::PossiblyNotCallable => {

View file

@ -9,7 +9,9 @@ use crate::types::cyclic::PairVisitor;
use crate::types::enums::is_single_member_enum;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::TupleType;
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance};
use crate::types::{
DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, TypedDictType,
};
use crate::{Db, FxOrderSet};
pub(super) use synthesized_protocol::SynthesizedProtocolType;
@ -24,10 +26,16 @@ impl<'db> Type<'db> {
(ClassType::Generic(alias), Some(KnownClass::Tuple)) => {
Self::tuple(TupleType::new(db, alias.specialization(db).tuple(db)))
}
_ if class.class_literal(db).0.is_protocol(db) => {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
_ => {
let class_literal = class.class_literal(db).0;
if class_literal.is_protocol(db) {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
} else if class_literal.is_typed_dict(db) {
TypedDictType::from(db, class)
} else {
Self::NominalInstance(NominalInstanceType::from_class(class))
}
}
_ => Self::NominalInstance(NominalInstanceType::from_class(class)),
}
}