[ty] Skip unrelated TypedDict.get overloads for literal keys

This commit is contained in:
Charlie Marsh committed 2026-10-26 21:14:05 -07:00
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)
}