Skip to content

Commit ba00e77

Browse files
committed
Fix: Skip bad-return validation for Protocol methods (#1916)
Signed-off-by: Aryan Bagade <aryan@aryanbagade.com>
1 parent 44457a9 commit ba00e77

File tree

4 files changed

+49
-2
lines changed

4 files changed

+49
-2
lines changed

pyrefly/lib/alt/solve.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3102,6 +3102,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
31023102
annotation,
31033103
stub_or_impl,
31043104
decorators,
3105+
class_key,
31053106
implicit_return,
31063107
is_generator,
31073108
has_explicit_return,
@@ -3110,9 +3111,18 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
31103111
// It will result in an implicit Any type, which is reasonable, but we should
31113112
// at least error here.
31123113
let ty = self.get_idx(*annotation).annotation.get_type().clone();
3113-
// If the function body is stubbed out or if the function is decorated with
3114-
// `@abstractmethod`, we blindly accept the return type annotation.
3114+
// Check if this method is in a protocol class
3115+
let is_in_protocol = class_key.is_some_and(|key| {
3116+
self.get_idx(key)
3117+
.0
3118+
.as_ref()
3119+
.is_some_and(|cls| self.get_metadata_for_class(cls).is_protocol())
3120+
});
3121+
// If the function body is stubbed out, if the function is decorated with
3122+
// `@abstractmethod`, or if the function is a method in a protocol class,
3123+
// we blindly accept the return type annotation.
31153124
if *stub_or_impl != FunctionStubOrImpl::Stub
3125+
&& !is_in_protocol
31163126
&& !decorators.iter().any(|k| {
31173127
let decorator = self.get_idx(*k);
31183128
match decorator.ty.callee_kind() {

pyrefly/lib/binding/binding.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,8 @@ pub enum ReturnTypeKind {
11501150
/// We keep this just so we can scan for `@abstractmethod` and use the info to decide
11511151
/// whether to skip the validation.
11521152
decorators: Box<[Idx<KeyDecorator>]>,
1153+
/// The class this function belongs to, if any. Used to skip validation for protocol methods.
1154+
class_key: Option<Idx<KeyClass>>,
11531155
implicit_return: Idx<Key>,
11541156
is_generator: bool,
11551157
has_explicit_return: bool,

pyrefly/lib/binding/function.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ impl<'a> BindingsBuilder<'a> {
371371
should_infer_return_type: bool,
372372
stub_or_impl: FunctionStubOrImpl,
373373
decorators: Box<[Idx<KeyDecorator>]>,
374+
class_key: Option<Idx<KeyClass>>,
374375
) {
375376
let is_generator =
376377
!(yields_and_returns.yields.is_empty() && yields_and_returns.yield_froms.is_empty());
@@ -431,6 +432,7 @@ impl<'a> BindingsBuilder<'a> {
431432
annotation,
432433
stub_or_impl,
433434
decorators,
435+
class_key,
434436
implicit_return,
435437
is_generator: !(yield_keys.is_empty() && yield_from_keys.is_empty()),
436438
has_explicit_return: !return_keys.is_empty(),
@@ -613,6 +615,7 @@ impl<'a> BindingsBuilder<'a> {
613615
false, // this disables return type inference
614616
stub_or_impl,
615617
decorators.decorators.clone(),
618+
class_key,
616619
);
617620
self_assignments
618621
}
@@ -643,6 +646,7 @@ impl<'a> BindingsBuilder<'a> {
643646
false, // this disables return type inference
644647
stub_or_impl,
645648
decorators.decorators.clone(),
649+
class_key,
646650
);
647651
self_assignments
648652
}
@@ -673,6 +677,7 @@ impl<'a> BindingsBuilder<'a> {
673677
true,
674678
stub_or_impl,
675679
decorators.decorators.clone(),
680+
class_key,
676681
);
677682
self_assignments
678683
}

pyrefly/lib/test/protocol.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,3 +847,33 @@ def test():
847847
p: TrickyProtocol[int] = t # E:
848848
"#,
849849
);
850+
851+
// Regression test for https://github.com/facebook/pyrefly/issues/1916
852+
// Protocol methods with only a docstring should not emit "missing explicit return" errors
853+
testcase!(
854+
test_protocol_method_with_docstring,
855+
r#"
856+
from typing import Protocol
857+
858+
class SortState:
859+
pass
860+
861+
class View(Protocol):
862+
"""A protocol with methods that have docstrings but no body."""
863+
864+
@property
865+
def sort_state(self) -> SortState:
866+
"""Return the current sorting/grouping settings."""
867+
868+
def get_value(self) -> int:
869+
"""Get a value."""
870+
871+
def method_with_ellipsis(self) -> str:
872+
"""This one uses ellipsis."""
873+
...
874+
875+
def method_with_pass(self) -> str:
876+
"""This one uses pass - should still be allowed in protocol."""
877+
pass
878+
"#,
879+
);

0 commit comments

Comments
 (0)