[ty] Do not assume that fields have a default value (#20914)

## Summary

fixes https://github.com/astral-sh/ty/issues/1366

## Test Plan

Added regression test
This commit is contained in:
David Peter 2025-10-16 12:49:24 +02:00 committed by GitHub
parent c9dfb51f49
commit 0cc663efcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 29 additions and 19 deletions

View file

@ -497,6 +497,8 @@ class A:
a: str = field(kw_only=False) a: str = field(kw_only=False)
b: int = 0 b: int = 0
reveal_type(A.__init__) # revealed: (self: A, a: str, *, b: int = Literal[0]) -> None
A("hi") A("hi")
``` ```

View file

@ -108,7 +108,7 @@ class A:
name: str = field(init=False) name: str = field(init=False)
# field(init=False) should be ignored for dataclass_transform without explicit field_specifiers # 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 @dataclass
class B: class B:

View file

@ -1636,14 +1636,16 @@ impl<'db> Type<'db> {
(Type::KnownInstance(KnownInstanceType::Field(field)), right) (Type::KnownInstance(KnownInstanceType::Field(field)), right)
if relation.is_assignability() => if relation.is_assignability() =>
{ {
field.default_type(db).has_relation_to_impl( field.default_type(db).when_none_or(|default_type| {
db, default_type.has_relation_to_impl(
right, db,
inferable, right,
relation, inferable,
relation_visitor, relation,
disjointness_visitor, relation_visitor,
) disjointness_visitor,
)
})
} }
// Dynamic is only a subtype of `object` and only a supertype of `Never`; both were // 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 // Nothing to visit
} }
KnownInstanceType::Field(field) => { 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::TypeVar(_) => f.write_str("typing.TypeVar"),
KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"), KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"),
KnownInstanceType::Field(field) => { KnownInstanceType::Field(field) => {
f.write_str("dataclasses.Field[")?; f.write_str("dataclasses.Field")?;
field.default_type(self.db).display(self.db).fmt(f)?; if let Some(default_ty) = field.default_type(self.db) {
f.write_str("]") write!(f, "[{}]", default_ty.display(self.db))?;
}
Ok(())
} }
KnownInstanceType::ConstraintSet(tracked_set) => { KnownInstanceType::ConstraintSet(tracked_set) => {
let constraints = tracked_set.constraints(self.db); let constraints = tracked_set.constraints(self.db);
@ -7988,7 +7994,7 @@ impl get_size2::GetSize for DeprecatedInstance<'_> {}
pub struct FieldInstance<'db> { pub struct FieldInstance<'db> {
/// The type of the default value for this field. This is derived from the `default` or /// The type of the default value for this field. This is derived from the `default` or
/// `default_factory` arguments to `dataclasses.field()`. /// `default_factory` arguments to `dataclasses.field()`.
pub default_type: Type<'db>, pub default_type: Option<Type<'db>>,
/// Whether this field is part of the `__init__` signature, or not. /// Whether this field is part of the `__init__` signature, or not.
pub init: bool, 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 { pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
FieldInstance::new( FieldInstance::new(
db, db,
self.default_type(db).normalized_impl(db, visitor), self.default_type(db)
.map(|ty| ty.normalized_impl(db, visitor)),
self.init(db), self.init(db),
self.kw_only(db), self.kw_only(db),
) )

View file

@ -978,11 +978,12 @@ impl<'db> Bindings<'db> {
overload.parameter_type_by_name("kw_only").unwrap_or(None); overload.parameter_type_by_name("kw_only").unwrap_or(None);
let default_ty = match (default, default_factory) { 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 (_, Some(default_factory_ty)) => default_factory_ty
.try_call(db, &CallArguments::none()) .try_call(db, &CallArguments::none())
.map_or(Type::unknown(), |binding| binding.return_type(db)), .ok()
_ => Type::unknown(), .map(|binding| binding.return_type(db)),
_ => None,
}; };
let init = init let init = init

View file

@ -2898,7 +2898,7 @@ impl<'db> ClassLiteral<'db> {
let mut init = true; let mut init = true;
let mut kw_only = None; let mut kw_only = None;
if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty { 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 if self
.dataclass_params(db) .dataclass_params(db)
.map(|params| params.contains(DataclassParams::NO_FIELD_SPECIFIERS)) .map(|params| params.contains(DataclassParams::NO_FIELD_SPECIFIERS))