mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[ty] Skip unrelated TypedDict.get overloads for literal keys
This commit is contained in:
1 parent
c9caebb2db
commit
9f6e23755a
4 files changed
+74
-8
No files matched your search
@@ -413,11 +413,7 @@ fn synthesize_typed_dict_get<'db>(
|
||||
];
|
||||
let get_sig = Signature::new(
|
||||
Parameters::standard(get_sig_params),
|
||||
if field.is_required() {
|
||||
field.declared_ty
|
||||
} else {
|
||||
UnionType::from_two_elements(db, env, field.declared_ty, Type::none(db, env))
|
||||
},
|
||||
field.get_type_without_default(db, env),
|
||||
);
|
||||
|
||||
let t_default = BoundTypeVarInstance::synthetic(
|
||||
|
||||
@@ -9547,6 +9547,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// arguments after matching them to parameters, but before checking that the argument types
|
||||
// are assignable to any parameter annotations.
|
||||
let mut call_arguments = self.prepare_call_arguments(arguments);
|
||||
let mut callable_to_bind = callable_type;
|
||||
|
||||
// Special handling for `TypedDict` method calls
|
||||
if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
|
||||
@@ -9586,6 +9587,40 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
return Type::unknown();
|
||||
}
|
||||
|
||||
// A key inferred as a string literal can only select this field's
|
||||
// overloads or a string fallback. Open files keep all overload
|
||||
// contexts for key completions.
|
||||
if is_declared
|
||||
&& method_name == "get"
|
||||
&& arguments.keywords.is_empty()
|
||||
&& first_arg.is_string_literal_expr()
|
||||
&& key.len() <= Self::MAX_STRING_LITERAL_SIZE
|
||||
&& !self.collects_expected_types()
|
||||
{
|
||||
if arguments.args.len() == 1 {
|
||||
self.get_or_infer_expression(
|
||||
first_arg,
|
||||
TypeContext::declared(Some(Type::string_literal(db, key))),
|
||||
);
|
||||
return field.get_type_without_default(db, env);
|
||||
}
|
||||
if let [_, default] = arguments.args.as_ref()
|
||||
&& items.len() > 1
|
||||
&& !default.is_starred_expr()
|
||||
&& let Type::BoundMethod(method) = callable_type
|
||||
&& let Type::Callable(callable) = method.func(db)
|
||||
{
|
||||
let key_ty = Type::string_literal(db, key);
|
||||
let overloads = callable.signatures(db).iter().filter(|overload| {
|
||||
let param_ty = overload.parameters()[1].annotated_type();
|
||||
param_ty == key_ty || param_ty.as_string_literal().is_none()
|
||||
});
|
||||
let signatures = CallableSignature::from_overloads(overloads.cloned());
|
||||
let function = Type::Callable(callable.with_signatures(db, signatures));
|
||||
callable_to_bind = Type::BoundMethod(method.with_func(db, function));
|
||||
}
|
||||
}
|
||||
|
||||
if !is_declared
|
||||
&& method_name == "get"
|
||||
&& arguments.keywords.is_empty()
|
||||
@@ -9860,7 +9895,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
let mut bindings =
|
||||
self.bindings_for_call(callable_type)
|
||||
self.bindings_for_call(callable_to_bind)
|
||||
.match_parameters(db, env, &call_arguments);
|
||||
|
||||
report_missing_implicit_constructor_call(
|
||||
|
||||
@@ -305,6 +305,28 @@ fn not_literal_string() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_dict_get_with_a_widened_string_key() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
&format!(
|
||||
r#"
|
||||
from typing import TypedDict, reveal_type
|
||||
|
||||
class Record(TypedDict):
|
||||
{key}: int
|
||||
|
||||
def read(record: Record):
|
||||
reveal_type(record.get("{key}"))
|
||||
"#,
|
||||
key = "x".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1),
|
||||
),
|
||||
)?;
|
||||
assert_revealed_type(&db, "src/a.py", "Unknown | None");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiplied_string() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
@@ -19,8 +19,8 @@ use super::diagnostic::{
|
||||
use super::infer::{TypeExpressionFlags, infer_deferred_types};
|
||||
use super::{
|
||||
ApplyTypeMappingVisitor, BoundTypeVarIdentity, ErrorContext, IntersectionType, Type,
|
||||
TypeMapping, TypeQualifiers, TypeVarVariance, UnionBuilder, VarianceInferable, VarianceTerm,
|
||||
definition_expression_annotation, definition_expression_type, visitor,
|
||||
TypeMapping, TypeQualifiers, TypeVarVariance, UnionBuilder, UnionType, VarianceInferable,
|
||||
VarianceTerm, definition_expression_annotation, definition_expression_type, visitor,
|
||||
};
|
||||
use crate::types::TypeContext;
|
||||
use crate::types::TypeDefinition;
|
||||
@@ -3251,6 +3251,19 @@ impl<'db> TypedDictField<'db> {
|
||||
self.flags.contains(TypedDictFieldFlags::REQUIRED)
|
||||
}
|
||||
|
||||
/// Returns the type of `TypedDict.get` for this field when no default is passed.
|
||||
pub(super) fn get_type_without_default(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
env: &ProgramEnvironment<'db>,
|
||||
) -> Type<'db> {
|
||||
if self.is_required() {
|
||||
self.declared_ty
|
||||
} else {
|
||||
UnionType::from_two_elements(db, env, self.declared_ty, Type::none(db, env))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_read_only(&self) -> bool {
|
||||
self.flags.contains(TypedDictFieldFlags::READ_ONLY)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user