From 0cc663efcd0ff2bc81f34ff3a658840f01f2a697 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 16 Oct 2025 12:49:24 +0200 Subject: [PATCH] [ty] Do not assume that `field`s have a default value (#20914) ## Summary fixes https://github.com/astral-sh/ty/issues/1366 ## Test Plan Added regression test --- .../mdtest/dataclasses/dataclasses.md | 2 ++ .../resources/mdtest/dataclasses/fields.md | 2 +- crates/ty_python_semantic/src/types.rs | 35 +++++++++++-------- .../ty_python_semantic/src/types/call/bind.rs | 7 ++-- crates/ty_python_semantic/src/types/class.rs | 2 +- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 7eb2ff9d67..34899b10fc 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -497,6 +497,8 @@ class A: a: str = field(kw_only=False) b: int = 0 +reveal_type(A.__init__) # revealed: (self: A, a: str, *, b: int = Literal[0]) -> None + A("hi") ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md index 592190f942..d1ac6dc0be 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md @@ -108,7 +108,7 @@ class A: name: str = field(init=False) # field(init=False) should be ignored for dataclass_transform without explicit field_specifiers -reveal_type(A.__init__) # revealed: (self: A, name: str = Unknown) -> None +reveal_type(A.__init__) # revealed: (self: A, name: str) -> None @dataclass class B: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3cb6d43e5c..7f0edaa7b6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1636,14 +1636,16 @@ impl<'db> Type<'db> { (Type::KnownInstance(KnownInstanceType::Field(field)), right) if relation.is_assignability() => { - field.default_type(db).has_relation_to_impl( - db, - right, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + field.default_type(db).when_none_or(|default_type| { + default_type.has_relation_to_impl( + db, + right, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) } // Dynamic is only a subtype of `object` and only a supertype of `Never`; both were @@ -7499,7 +7501,9 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( // Nothing to visit } KnownInstanceType::Field(field) => { - visitor.visit_type(db, field.default_type(db)); + if let Some(default_ty) = field.default_type(db) { + visitor.visit_type(db, default_ty); + } } } } @@ -7599,9 +7603,11 @@ impl<'db> KnownInstanceType<'db> { KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"), KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"), KnownInstanceType::Field(field) => { - f.write_str("dataclasses.Field[")?; - field.default_type(self.db).display(self.db).fmt(f)?; - f.write_str("]") + f.write_str("dataclasses.Field")?; + if let Some(default_ty) = field.default_type(self.db) { + write!(f, "[{}]", default_ty.display(self.db))?; + } + Ok(()) } KnownInstanceType::ConstraintSet(tracked_set) => { let constraints = tracked_set.constraints(self.db); @@ -7988,7 +7994,7 @@ impl get_size2::GetSize for DeprecatedInstance<'_> {} pub struct FieldInstance<'db> { /// The type of the default value for this field. This is derived from the `default` or /// `default_factory` arguments to `dataclasses.field()`. - pub default_type: Type<'db>, + pub default_type: Option>, /// Whether this field is part of the `__init__` signature, or not. pub init: bool, @@ -8004,7 +8010,8 @@ impl<'db> FieldInstance<'db> { pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { FieldInstance::new( db, - self.default_type(db).normalized_impl(db, visitor), + self.default_type(db) + .map(|ty| ty.normalized_impl(db, visitor)), self.init(db), self.kw_only(db), ) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 128d61e1d5..abaa77cb49 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -978,11 +978,12 @@ impl<'db> Bindings<'db> { overload.parameter_type_by_name("kw_only").unwrap_or(None); let default_ty = match (default, default_factory) { - (Some(default_ty), _) => default_ty, + (Some(default_ty), _) => Some(default_ty), (_, Some(default_factory_ty)) => default_factory_ty .try_call(db, &CallArguments::none()) - .map_or(Type::unknown(), |binding| binding.return_type(db)), - _ => Type::unknown(), + .ok() + .map(|binding| binding.return_type(db)), + _ => None, }; let init = init diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 64ba611a46..da30aa3144 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2898,7 +2898,7 @@ impl<'db> ClassLiteral<'db> { let mut init = true; let mut kw_only = None; if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty { - default_ty = Some(field.default_type(db)); + default_ty = field.default_type(db); if self .dataclass_params(db) .map(|params| params.contains(DataclassParams::NO_FIELD_SPECIFIERS))