[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)
b: int = 0
reveal_type(A.__init__) # revealed: (self: A, a: str, *, b: int = Literal[0]) -> None
A("hi")
```

View file

@ -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:

View file

@ -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<Type<'db>>,
/// 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),
)

View file

@ -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

View file

@ -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))