Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/pyrefly_bundled/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ fn extract_pyi_files_from_archive(filter: PathFilter) -> anyhow::Result<SmallMap
let mut relative_path_components = relative_path_context.components();

let first_component = relative_path_components.next();
if let Some(expected) = filter.expected_first_component() {
if first_component.is_none_or(|component| component.as_os_str() != expected) {
continue;
}
if let Some(expected) = filter.expected_first_component()
&& first_component.is_none_or(|component| component.as_os_str() != expected)
{
continue;
}
// For ThirdPartyStubs, we need to put first_component back into the path

Expand Down
32 changes: 22 additions & 10 deletions crates/pyrefly_types/src/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,26 @@ impl Deprecation {
}
}

#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Visit, VisitMut, TypeEq
)]
pub enum PropertyRole {
Getter,
Setter,
SetterDecorator,
DeleterDecorator,
}

#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Visit, VisitMut, TypeEq
)]
pub struct PropertyMetadata {
pub role: PropertyRole,
pub getter: Type,
pub setter: Option<Type>,
pub has_deleter: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[derive(Visit, VisitMut, TypeEq)]
pub struct FuncFlags {
Expand All @@ -357,18 +377,10 @@ pub struct FuncFlags {
pub is_classmethod: bool,
/// A function decorated with `@deprecated`
pub deprecation: Option<Deprecation>,
/// A function decorated with `@property`
pub is_property_getter: bool,
/// Metadata for `@property`, `@foo.setter`, and `@foo.deleter`.
pub property_metadata: Option<PropertyMetadata>,
/// A function decorated with `functools.cached_property` or equivalent.
pub is_cached_property: bool,
/// A `foo.setter` function, where `foo` is some `@property`-decorated function.
/// When used to decorate a function, turns the decorated function into a property setter.
pub is_property_setter_decorator: bool,
/// If None, this is a function decorated with `@foo.setter`, where `foo` is
/// a property (i.e. a function decoratoed with `@property`)
///
/// The stored type is `foo` (the getter).
pub is_property_setter_with_getter: Option<Type>,
pub has_enum_member_decoration: bool,
pub is_override: bool,
pub has_final_decoration: bool,
Expand Down
32 changes: 29 additions & 3 deletions crates/pyrefly_types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ use crate::callable::FunctionKind;
use crate::callable::Param;
use crate::callable::ParamList;
use crate::callable::Params;
use crate::callable::PropertyMetadata;
use crate::callable::PropertyRole;
use crate::callable::Required;
use crate::class::Class;
use crate::class::ClassKind;
Expand Down Expand Up @@ -1136,20 +1138,44 @@ impl Type {
self.check_toplevel_func_metadata(&|meta| meta.flags.has_enum_member_decoration)
}

pub fn property_metadata(&self) -> Option<PropertyMetadata> {
self.check_toplevel_func_metadata(&|meta| meta.flags.property_metadata.clone())
}

pub fn is_property_getter(&self) -> bool {
self.check_toplevel_func_metadata(&|meta| meta.flags.is_property_getter)
self.property_metadata()
.is_some_and(|meta| matches!(meta.role, PropertyRole::Getter))
}

pub fn is_cached_property(&self) -> bool {
self.check_toplevel_func_metadata(&|meta| meta.flags.is_cached_property)
}

pub fn is_property_setter_decorator(&self) -> bool {
self.check_toplevel_func_metadata(&|meta| meta.flags.is_property_setter_decorator)
self.property_metadata()
.is_some_and(|meta| matches!(meta.role, PropertyRole::SetterDecorator))
}

pub fn is_property_setter_with_getter(&self) -> Option<Type> {
self.check_toplevel_func_metadata(&|meta| meta.flags.is_property_setter_with_getter.clone())
self.property_metadata().and_then(|meta| match meta.role {
PropertyRole::Setter => Some(meta.getter.clone()),
_ => None,
})
}

pub fn property_deleter_metadata(&self) -> Option<PropertyMetadata> {
self.property_metadata().and_then(|meta| match meta.role {
PropertyRole::DeleterDecorator => Some(meta),
_ => None,
})
}

pub fn without_property_metadata(&self) -> Type {
let mut clone = self.clone();
clone.transform_toplevel_func_metadata(|meta| {
meta.flags.property_metadata = None;
});
clone
}

pub fn is_overload(&self) -> bool {
Expand Down
61 changes: 55 additions & 6 deletions pyrefly/lib/alt/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ use crate::solver::solver::SubsetError;
use crate::types::callable::FuncMetadata;
use crate::types::callable::Function;
use crate::types::callable::FunctionKind;
use crate::types::callable::PropertyMetadata;
use crate::types::callable::PropertyRole;
use crate::types::class::Class;
use crate::types::class::ClassType;
use crate::types::literal::Lit;
Expand Down Expand Up @@ -409,6 +411,13 @@ impl InternalError {
#[derive(Clone, Debug)]
struct AttributeBase(Vec1<AttributeBase1>);

#[derive(Clone, Debug)]
struct PropertyAttr {
getter: Type,
setter: Option<Type>,
deleter: bool,
}

/// A single, "atomic" attribute base, not coming from a union or an intersection.
/// An attribute is either found or not found by a search on this.
#[derive(Clone, Debug)]
Expand All @@ -435,8 +444,8 @@ enum AttributeBase1 {
/// before falling back to `Never`.
TypeNever,
/// Properties are handled via a special case so that we can understand
/// setter decorators.
Property(Type),
/// setter/deleter decorators.
Property(PropertyAttr),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the PropertyPayload is unnecessary then this one is too

/// Attribute access on `Self` from inside a class
SelfType(ClassType),
/// Result of a super() call. See Type::SuperInstance for details on what these fields are.
Expand Down Expand Up @@ -1385,7 +1394,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}
}
}
AttributeBase1::Property(getter) => {
AttributeBase1::Property(property) => {
if attr_name == "setter" {
// When given a decorator `@some_property.setter`, instead of modeling the setter
// directly at the type level we just return the getter (the raw `some_property`)
Expand All @@ -1400,14 +1409,35 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
//
// TODO(stroxler): it is probably possible to synthesize a forall type here
// that uses a type var to propagate the setter. Investigate this option later.
let mut getter = getter.clone();
let mut getter = property.getter.clone();
let metadata_getter = property.getter.without_property_metadata();
let metadata_setter = property
.setter
.as_ref()
.map(|setter| setter.without_property_metadata());
getter.transform_toplevel_func_metadata(|meta: &mut FuncMetadata| {
meta.flags.is_property_setter_decorator = true;
meta.flags.property_metadata = Some(PropertyMetadata {
role: PropertyRole::SetterDecorator,
getter: metadata_getter.clone(),
setter: metadata_setter.clone(),
has_deleter: property.deleter,
});
});
acc.found_type(
// TODO(samzhou19815): Support go-to-definition for @property applied symbols
getter, base,
)
} else if attr_name == "deleter" {
let mut getter = property.getter.clone();
getter.transform_toplevel_func_metadata(|meta: &mut FuncMetadata| {
meta.flags.property_metadata = Some(PropertyMetadata {
role: PropertyRole::DeleterDecorator,
getter: property.getter.clone(),
setter: property.setter.clone(),
has_deleter: true,
});
});
acc.found_type(getter, base)
} else {
let class = self.stdlib.property();
match self.get_instance_attribute(class, attr_name) {
Expand Down Expand Up @@ -1845,7 +1875,26 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
ClassBase::ClassType(self.stdlib.method_type().clone()),
)),
Type::Never(_) => acc.push(AttributeBase1::Never),
_ if ty.is_property_getter() => acc.push(AttributeBase1::Property(ty)),
_ if ty.is_property_getter() => {
let deleter = ty
.property_metadata()
.map(|meta| meta.has_deleter)
.unwrap_or(false);
acc.push(AttributeBase1::Property(PropertyAttr {
getter: ty.clone(),
setter: None,
deleter,
}));
}
_ if let Some(metadata) = ty.property_metadata()
&& matches!(metadata.role, PropertyRole::Setter) =>
{
acc.push(AttributeBase1::Property(PropertyAttr {
getter: metadata.getter.clone(),
setter: Some(ty.clone()),
deleter: metadata.has_deleter,
}));
}
Type::Callable(_) => acc.push(AttributeBase1::ClassInstance(
self.stdlib.function_type().clone(),
)),
Expand Down
9 changes: 8 additions & 1 deletion pyrefly/lib/alt/class/django.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use pyrefly_types::callable::Callable;
use pyrefly_types::callable::FuncMetadata;
use pyrefly_types::callable::Function;
use pyrefly_types::callable::ParamList;
use pyrefly_types::callable::PropertyMetadata;
use pyrefly_types::callable::PropertyRole;
use pyrefly_types::class::Class;
use pyrefly_types::literal::Lit;
use pyrefly_types::tuple::Tuple;
Expand Down Expand Up @@ -310,7 +312,12 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
fn property(&self, cls: &Class, name: Name, ty: Type) -> Type {
let signature = Callable::list(ParamList::new(vec![self.class_self_param(cls, false)]), ty);
let mut metadata = FuncMetadata::def(self.module().dupe(), cls.dupe(), name);
metadata.flags.is_property_getter = true;
metadata.flags.property_metadata = Some(PropertyMetadata {
role: PropertyRole::Getter,
getter: Type::any_error(),
setter: None,
has_deleter: false,
});
Type::Function(Box::new(Function {
signature,
metadata,
Expand Down
84 changes: 71 additions & 13 deletions pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ use crate::types::callable::Function;
use crate::types::callable::FunctionKind;
use crate::types::callable::Param;
use crate::types::callable::ParamList;
use crate::types::callable::PropertyMetadata;
use crate::types::callable::PropertyRole;
use crate::types::callable::Required;
use crate::types::class::ClassKind;
use crate::types::keywords::DataclassTransformMetadata;
Expand Down Expand Up @@ -317,6 +319,24 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}
};

if let Some(metadata) = def
.metadata()
.flags
.property_metadata
.as_ref()
.filter(|meta| matches!(meta.role, PropertyRole::DeleterDecorator))
{
ty = metadata
.setter
.clone()
.unwrap_or_else(|| metadata.getter.clone());
ty.transform_toplevel_func_metadata(|meta| {
if let Some(property) = &mut meta.flags.property_metadata {
property.has_deleter = true;
}
});
}

if def.is_stub()
&& self.module().path().style() != ModuleStyle::Interface
&& let Some(cls) = def.defining_cls()
Expand All @@ -327,6 +347,21 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
});
}

let sanitized = ty.without_property_metadata();
ty.transform_toplevel_func_metadata(|meta| {
if let Some(property) = &mut meta.flags.property_metadata {
match property.role {
PropertyRole::Getter => {
property.getter = sanitized.clone();
}
PropertyRole::Setter => {
property.setter = Some(sanitized.clone());
}
_ => {}
}
}
});

ty
}

Expand Down Expand Up @@ -549,6 +584,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
&'a self,
decorator: &'a Decorator,
) -> Option<SpecialDecorator<'a>> {
if decorator
.ty
.property_metadata()
.is_some_and(|meta| matches!(meta.role, PropertyRole::DeleterDecorator))
{
return Some(SpecialDecorator::PropertyDeleter(&decorator.ty));
}
match decorator.ty.callee_kind() {
Some(CalleeKind::Function(FunctionKind::Overload)) => Some(SpecialDecorator::Overload),
Some(CalleeKind::Class(ClassKind::StaticMethod(name))) => {
Expand All @@ -569,7 +611,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
_ if let Some(deprecation) = &decorator.deprecation => {
Some(SpecialDecorator::Deprecated(deprecation))
}
_ if decorator.ty.is_property_setter_decorator() => {
_ if decorator
.ty
.property_metadata()
.is_some_and(|meta| matches!(meta.role, PropertyRole::SetterDecorator)) =>
{
Some(SpecialDecorator::PropertySetter(&decorator.ty))
}
_ if let Type::KwCall(call) = &decorator.ty
Expand Down Expand Up @@ -607,11 +653,21 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
true
}
SpecialDecorator::Property(_) => {
flags.is_property_getter = true;
flags.property_metadata = Some(PropertyMetadata {
role: PropertyRole::Getter,
getter: Type::any_error(),
setter: None,
has_deleter: false,
});
true
}
SpecialDecorator::CachedProperty(_) => {
flags.is_property_getter = true;
flags.property_metadata = Some(PropertyMetadata {
role: PropertyRole::Getter,
getter: Type::any_error(),
setter: None,
has_deleter: false,
});
flags.is_cached_property = true;
true
}
Expand All @@ -632,16 +688,18 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
true
}
SpecialDecorator::PropertySetter(decorator) => {
// When the `setter` attribute is accessed on a property, we return the type
// of the raw getter function, but with the `is_property_setter_decorator`
// flag set to true; the type does does not accurately model the runtime
// (calling the `.setter` decorator does not invoke a getter function),
// but makes it convenient to construct the property getter and setter
// in our class field logic.
//
// See AnswersSolver::lookup_attr_from_attribute_base
// for details.
flags.is_property_setter_with_getter = Some((*decorator).clone());
if let Some(metadata) = decorator.property_metadata() {
flags.property_metadata = Some(PropertyMetadata {
role: PropertyRole::Setter,
getter: metadata.getter.clone(),
setter: metadata.setter.clone(),
has_deleter: metadata.has_deleter,
});
}
true
}
SpecialDecorator::PropertyDeleter(decorator) => {
flags.property_metadata = decorator.property_metadata();
true
}
SpecialDecorator::DataclassTransformCall(kws) => {
Expand Down
1 change: 1 addition & 0 deletions pyrefly/lib/alt/types/decorated_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pub enum SpecialDecorator<'a> {
Final,
Deprecated(&'a Deprecation),
PropertySetter(&'a Type),
PropertyDeleter(&'a Type),
DataclassTransformCall(&'a TypeMap),
EnumNonmember,
AbstractMethod,
Expand Down
Loading