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
49 changes: 49 additions & 0 deletions pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,55 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
);
}

// A property getter runs with just the instance, and a setter with the instance
// and the assigned value, so any further required parameter is unreachable at
// runtime. The decorator path attaches property metadata without checking the
// signature against `property.__init__`, so validate it here.
// See https://github.com/facebook/pyrefly/issues/4918.
// Skip when custom (non-special) decorators remain between `@property` and
// the function, since they may reshape the signature before `@property`
// sees it (e.g. homeassistant Spotify's `@ensure_item` turns
// `(self, item)` into `(self)`). Special decorators like `@override`
// are already filtered out of `decorators`, so they don't suppress this.
if defining_cls.is_some()
&& !flags.is_staticmethod
&& !flags.is_classmethod
&& decorators.is_empty()
&& let Some(property) = &flags.property_metadata
{
let (kind, expected) = match property.role {
PropertyRole::Getter => ("getter", 1),
PropertyRole::Setter | PropertyRole::SetterDecorator => ("setter", 2),
PropertyRole::DeleterDecorator => ("deleter", 1),
};
let extra = def
.parameters
.posonlyargs
.iter()
.chain(def.parameters.args.iter())
.filter(|p| p.default.is_none())
.nth(expected)
.map(|p| &p.parameter)
.or_else(|| {
def.parameters
.kwonlyargs
.iter()
.find(|p| p.default.is_none())
.map(|p| &p.parameter)
});
if let Some(param) = extra {
self.error(
errors,
param.range,
ErrorKind::BadFunctionDefinition,
format!(
"Property {kind} cannot take extra required parameter `{}`",
param.name
),
);
}
}

let FunctionParamsResult {
params,
paramspec,
Expand Down
65 changes: 65 additions & 0 deletions pyrefly/lib/test/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,71 @@ def f(c: C) -> None:
"#,
);

testcase!(
test_property_getter_with_extra_required_parameter,
r#"
class Foo:
@property
def value(self, huh: str) -> int: # E: Property getter cannot take extra required parameter `huh`
return 1
"#,
);

testcase!(
test_property_setter_with_extra_required_parameter,
r#"
class Foo:
@property
def value(self) -> int:
return 1

@value.setter
def value(self, new_value: int, huh: str) -> None: # E: Property setter cannot take extra required parameter `huh`
pass
"#,
);

testcase!(
test_property_deleter_with_extra_required_parameter,
r#"
class Foo:
@property
def value(self) -> int:
return 1

@value.deleter
def value(self, huh: str) -> None: # E: Property deleter cannot take extra required parameter `huh`
pass
"#,
);

testcase!(
test_property_getter_with_defaulted_extra_parameter,
r#"
class Foo:
@property
def value(self, huh: str = "x") -> int:
return 1
"#,
);

testcase!(
test_property_getter_with_signature_changing_decorator,
r#"
from collections.abc import Callable
from typing import Any

def ensure_item(func: Callable[..., Any]) -> Callable[..., Any]:
return func

class Foo:
@property
@ensure_item
def value(self, item: int) -> int:
return item
"#,
);

testcase!(
test_cached_property_assignment_allowed,
r#"
Expand Down
Loading