mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[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:
parent
e917d309f1
commit
4887bdf205
13 changed files with 489 additions and 158 deletions
|
@ -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>>;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue