mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 20:24:27 +00:00
[ty] dict is not assignable to TypedDict (#21238)
## Summary A lot of the bidirectional inference work relies on `dict` not being assignable to `TypedDict`, so I think it makes sense to add this before fully implementing https://github.com/astral-sh/ty/issues/1387.
This commit is contained in:
parent
42adfd40ea
commit
3c8fb68765
11 changed files with 169 additions and 75 deletions
|
|
@ -1987,11 +1987,14 @@ impl<'db> Type<'db> {
|
|||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
(Type::TypedDict(_), _) => {
|
||||
// TODO: Implement assignability and subtyping for TypedDict
|
||||
ConstraintSet::from(relation.is_assignability())
|
||||
}
|
||||
|
||||
// A non-`TypedDict` cannot subtype a `TypedDict`
|
||||
(_, Type::TypedDict(_)) => ConstraintSet::from(false),
|
||||
|
||||
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
|
||||
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
|
||||
(left, Type::AlwaysFalsy) => ConstraintSet::from(left.bool(db).is_always_false()),
|
||||
|
|
|
|||
|
|
@ -3582,6 +3582,11 @@ impl<'db> BindingError<'db> {
|
|||
expected_ty,
|
||||
provided_ty,
|
||||
} => {
|
||||
// TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments
|
||||
// here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
|
||||
// silenced diagnostics during overload evaluation, and rely on the assignability
|
||||
// diagnostic being emitted here.
|
||||
|
||||
let range = Self::get_node(node, *argument_index);
|
||||
let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -2003,6 +2003,20 @@ pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeR
|
|||
builder.into_diagnostic("Slice step size cannot be zero");
|
||||
}
|
||||
|
||||
// We avoid emitting invalid assignment diagnostic for literal assignments to a `TypedDict`, as
|
||||
// they can only occur if we already failed to validate the dict (and emitted some diagnostic).
|
||||
pub(crate) fn is_invalid_typed_dict_literal(
|
||||
db: &dyn Db,
|
||||
target_ty: Type,
|
||||
source: AnyNodeRef<'_>,
|
||||
) -> bool {
|
||||
target_ty
|
||||
.filter_union(db, Type::is_typed_dict)
|
||||
.as_typed_dict()
|
||||
.is_some()
|
||||
&& matches!(source, AnyNodeRef::ExprDict(_))
|
||||
}
|
||||
|
||||
fn report_invalid_assignment_with_message(
|
||||
context: &InferContext,
|
||||
node: AnyNodeRef,
|
||||
|
|
@ -2040,15 +2054,27 @@ pub(super) fn report_invalid_assignment<'db>(
|
|||
target_ty: Type,
|
||||
mut source_ty: Type<'db>,
|
||||
) {
|
||||
let value_expr = match definition.kind(context.db()) {
|
||||
DefinitionKind::Assignment(def) => Some(def.value(context.module())),
|
||||
DefinitionKind::AnnotatedAssignment(def) => def.value(context.module()),
|
||||
DefinitionKind::NamedExpression(def) => Some(&*def.node(context.module()).value),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(value_expr) = value_expr
|
||||
&& is_invalid_typed_dict_literal(context.db(), target_ty, value_expr.into())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let settings =
|
||||
DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, source_ty);
|
||||
|
||||
if let DefinitionKind::AnnotatedAssignment(annotated_assignment) = definition.kind(context.db())
|
||||
&& let Some(value) = annotated_assignment.value(context.module())
|
||||
{
|
||||
if let Some(value_expr) = value_expr {
|
||||
// Re-infer the RHS of the annotated assignment, ignoring the type context for more precise
|
||||
// error messages.
|
||||
source_ty = infer_isolated_expression(context.db(), definition.scope(context.db()), value);
|
||||
source_ty =
|
||||
infer_isolated_expression(context.db(), definition.scope(context.db()), value_expr);
|
||||
}
|
||||
|
||||
report_invalid_assignment_with_message(
|
||||
|
|
@ -2070,6 +2096,11 @@ pub(super) fn report_invalid_attribute_assignment(
|
|||
source_ty: Type,
|
||||
attribute_name: &'_ str,
|
||||
) {
|
||||
// TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments
|
||||
// here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
|
||||
// silenced diagnostics during attribute resolution, and rely on the assignability
|
||||
// diagnostic being emitted here.
|
||||
|
||||
report_invalid_assignment_with_message(
|
||||
context,
|
||||
node,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
|
|||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, PythonVersion};
|
||||
use ruff_python_ast::{
|
||||
self as ast, AnyNodeRef, ExprContext, HasNodeIndex, NodeIndex, PythonVersion,
|
||||
};
|
||||
use ruff_python_stdlib::builtins::version_builtin_was_added;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
|
@ -5859,15 +5861,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
expression.map(|expr| self.infer_expression(expr, tcx))
|
||||
}
|
||||
|
||||
fn get_or_infer_expression(
|
||||
&mut self,
|
||||
expression: &ast::Expr,
|
||||
tcx: TypeContext<'db>,
|
||||
) -> Type<'db> {
|
||||
self.try_expression_type(expression)
|
||||
.unwrap_or_else(|| self.infer_expression(expression, tcx))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn infer_expression(&mut self, expression: &ast::Expr, tcx: TypeContext<'db>) -> Type<'db> {
|
||||
debug_assert!(
|
||||
|
|
@ -6223,7 +6216,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
} = list;
|
||||
|
||||
let elts = elts.iter().map(|elt| [Some(elt)]);
|
||||
self.infer_collection_literal(elts, tcx, KnownClass::List)
|
||||
let infer_elt_ty = |builder: &mut Self, elt, tcx| builder.infer_expression(elt, tcx);
|
||||
self.infer_collection_literal(elts, tcx, infer_elt_ty, KnownClass::List)
|
||||
.unwrap_or_else(|| {
|
||||
KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()])
|
||||
})
|
||||
|
|
@ -6237,7 +6231,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
} = set;
|
||||
|
||||
let elts = elts.iter().map(|elt| [Some(elt)]);
|
||||
self.infer_collection_literal(elts, tcx, KnownClass::Set)
|
||||
let infer_elt_ty = |builder: &mut Self, elt, tcx| builder.infer_expression(elt, tcx);
|
||||
self.infer_collection_literal(elts, tcx, infer_elt_ty, KnownClass::Set)
|
||||
.unwrap_or_else(|| {
|
||||
KnownClass::Set.to_specialized_instance(self.db(), [Type::unknown()])
|
||||
})
|
||||
|
|
@ -6250,12 +6245,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
items,
|
||||
} = dict;
|
||||
|
||||
let mut item_types = FxHashMap::default();
|
||||
|
||||
// Validate `TypedDict` dictionary literal assignments.
|
||||
if let Some(tcx) = tcx.annotation
|
||||
&& let Some(typed_dict) = tcx
|
||||
.filter_union(self.db(), Type::is_typed_dict)
|
||||
.as_typed_dict()
|
||||
&& let Some(ty) = self.infer_typed_dict_expression(dict, typed_dict)
|
||||
&& let Some(ty) = self.infer_typed_dict_expression(dict, typed_dict, &mut item_types)
|
||||
{
|
||||
return ty;
|
||||
}
|
||||
|
|
@ -6271,7 +6268,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
.iter()
|
||||
.map(|item| [item.key.as_ref(), Some(&item.value)]);
|
||||
|
||||
self.infer_collection_literal(items, tcx, KnownClass::Dict)
|
||||
// Avoid inferring the items multiple times if we already attempted to infer the
|
||||
// dictionary literal as a `TypedDict`. This also allows us to infer using the
|
||||
// type context of the expected `TypedDict` field.
|
||||
let infer_elt_ty = |builder: &mut Self, elt: &ast::Expr, tcx| {
|
||||
item_types
|
||||
.get(&elt.node_index().load())
|
||||
.copied()
|
||||
.unwrap_or_else(|| builder.infer_expression(elt, tcx))
|
||||
};
|
||||
|
||||
self.infer_collection_literal(items, tcx, infer_elt_ty, KnownClass::Dict)
|
||||
.unwrap_or_else(|| {
|
||||
KnownClass::Dict
|
||||
.to_specialized_instance(self.db(), [Type::unknown(), Type::unknown()])
|
||||
|
|
@ -6282,6 +6289,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
&mut self,
|
||||
dict: &ast::ExprDict,
|
||||
typed_dict: TypedDictType<'db>,
|
||||
item_types: &mut FxHashMap<NodeIndex, Type<'db>>,
|
||||
) -> Option<Type<'db>> {
|
||||
let ast::ExprDict {
|
||||
range: _,
|
||||
|
|
@ -6293,14 +6301,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
for item in items {
|
||||
let key_ty = self.infer_optional_expression(item.key.as_ref(), TypeContext::default());
|
||||
if let Some((key, key_ty)) = item.key.as_ref().zip(key_ty) {
|
||||
item_types.insert(key.node_index().load(), key_ty);
|
||||
}
|
||||
|
||||
if let Some(Type::StringLiteral(key)) = key_ty
|
||||
let value_ty = if let Some(Type::StringLiteral(key)) = key_ty
|
||||
&& let Some(field) = typed_dict_items.get(key.value(self.db()))
|
||||
{
|
||||
self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)));
|
||||
self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)))
|
||||
} else {
|
||||
self.infer_expression(&item.value, TypeContext::default());
|
||||
}
|
||||
self.infer_expression(&item.value, TypeContext::default())
|
||||
};
|
||||
|
||||
item_types.insert(item.value.node_index().load(), value_ty);
|
||||
}
|
||||
|
||||
validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| {
|
||||
|
|
@ -6311,12 +6324,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
|
||||
// Infer the type of a collection literal expression.
|
||||
fn infer_collection_literal<'expr, const N: usize>(
|
||||
fn infer_collection_literal<'expr, const N: usize, F, I>(
|
||||
&mut self,
|
||||
elts: impl Iterator<Item = [Option<&'expr ast::Expr>; N]>,
|
||||
elts: I,
|
||||
tcx: TypeContext<'db>,
|
||||
mut infer_elt_expression: F,
|
||||
collection_class: KnownClass,
|
||||
) -> Option<Type<'db>> {
|
||||
) -> Option<Type<'db>>
|
||||
where
|
||||
I: Iterator<Item = [Option<&'expr ast::Expr>; N]>,
|
||||
F: FnMut(&mut Self, &'expr ast::Expr, TypeContext<'db>) -> Type<'db>,
|
||||
{
|
||||
// Extract the type variable `T` from `list[T]` in typeshed.
|
||||
let elt_tys = |collection_class: KnownClass| {
|
||||
let class_literal = collection_class.try_to_class_literal(self.db())?;
|
||||
|
|
@ -6332,7 +6350,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
// Infer the element types without type context, and fallback to unknown for
|
||||
// custom typesheds.
|
||||
for elt in elts.flatten().flatten() {
|
||||
self.get_or_infer_expression(elt, TypeContext::default());
|
||||
infer_elt_expression(self, elt, TypeContext::default());
|
||||
}
|
||||
|
||||
return None;
|
||||
|
|
@ -6387,7 +6405,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
for elts in elts {
|
||||
// An unpacking expression for a dictionary.
|
||||
if let &[None, Some(value)] = elts.as_slice() {
|
||||
let inferred_value_ty = self.get_or_infer_expression(value, TypeContext::default());
|
||||
let inferred_value_ty = infer_elt_expression(self, value, TypeContext::default());
|
||||
|
||||
// Merge the inferred type of the nested dictionary.
|
||||
if let Some(specialization) =
|
||||
|
|
@ -6410,7 +6428,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
{
|
||||
let Some(elt) = elt else { continue };
|
||||
|
||||
let inferred_elt_ty = self.get_or_infer_expression(elt, elt_tcx);
|
||||
let inferred_elt_ty = infer_elt_expression(self, elt, elt_tcx);
|
||||
|
||||
// Simplify the inference based on the declared type of the element.
|
||||
if let Some(elt_tcx) = elt_tcx.annotation {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use ruff_text_size::Ranged;
|
|||
use super::class::{ClassType, CodeGeneratorKind, Field};
|
||||
use super::context::InferContext;
|
||||
use super::diagnostic::{
|
||||
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, report_invalid_key_on_typed_dict,
|
||||
self, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, report_invalid_key_on_typed_dict,
|
||||
report_missing_typed_dict_key,
|
||||
};
|
||||
use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
|
||||
|
|
@ -213,9 +213,13 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>(
|
|||
return true;
|
||||
}
|
||||
|
||||
let value_node = value_node.into();
|
||||
if diagnostic::is_invalid_typed_dict_literal(context.db(), item.declared_ty, value_node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Invalid assignment - emit diagnostic
|
||||
if let Some(builder) = context.report_lint(assignment_kind.diagnostic_type(), value_node.into())
|
||||
{
|
||||
if let Some(builder) = context.report_lint(assignment_kind.diagnostic_type(), value_node) {
|
||||
let typed_dict_ty = Type::TypedDict(typed_dict);
|
||||
let typed_dict_d = typed_dict_ty.display(db);
|
||||
let value_d = value_ty.display(db);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue