diff --git a/crates/pyrefly_config/src/error_kind.rs b/crates/pyrefly_config/src/error_kind.rs index 79cd48749f..b4a7433e22 100644 --- a/crates/pyrefly_config/src/error_kind.rs +++ b/crates/pyrefly_config/src/error_kind.rs @@ -257,6 +257,8 @@ pub enum ErrorKind { UnannotatedAttribute, /// A function parameter is missing a type annotation. UnannotatedParameter, + /// A protocol member is assigned a value in the class body without an explicit type annotation. + UnannotatedProtocolMember, /// A function is missing a return type annotation. UnannotatedReturn, /// Attempting to use a name that may be unbound or uninitialized diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 92bddea4d0..b74aab5899 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -1588,6 +1588,20 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { .. } => { let direct_annotation = annot.map(|a| self.get_idx(a).annotation.clone()); + if metadata.is_protocol() + && direct_annotation.is_none() + && !is_dunder(name.as_str()) + { + self.error( + errors, + range, + ErrorInfo::Kind(ErrorKind::UnannotatedProtocolMember), + format!( + "Protocol member `{}` must have an explicit type annotation", + name, + ), + ); + } let initialization = if let ExprOrBinding::Expr(e) = value.as_ref() && let Some(dm) = metadata.dataclass_metadata() && let Expr::Call(call) = e diff --git a/pyrefly/lib/test/protocol.rs b/pyrefly/lib/test/protocol.rs index 5adb9fc39a..ea8b6600a9 100644 --- a/pyrefly/lib/test/protocol.rs +++ b/pyrefly/lib/test/protocol.rs @@ -1008,15 +1008,16 @@ def to_foo() -> Foo[MySeries]: // https://github.com/facebook/pyrefly/issues/2925 testcase!( - bug = "Should detect ambiguous protocol members with value assignments", test_protocol_ambiguous_member, r#" from typing import Protocol class Ambiguous(Protocol): - # Assigning a value in a Protocol body is ambiguous: is it declaring - # a member with a type, or providing a default value? - x = None - y = ... + x = None # E: Protocol member `x` must have an explicit type annotation + y = ... # E: Protocol member `y` must have an explicit type annotation + +class Ok(Protocol): + x: int + y: str = "default" "#, ); diff --git a/website/docs/error-kinds.mdx b/website/docs/error-kinds.mdx index 19f9878af4..b7a559d894 100644 --- a/website/docs/error-kinds.mdx +++ b/website/docs/error-kinds.mdx @@ -1254,6 +1254,21 @@ def process_data(x: int, y: int) -> int: return x + y ``` +## unannotated-protocol-member + +This error is raised when a protocol member is assigned a value in the class body without an explicit type annotation. Protocol members must have explicitly declared types so that implementations know exactly what type to provide. + +```python +from typing import Protocol + +class MyProto(Protocol): + x = None # error: Protocol member `x` must have an explicit type annotation + +# Fixed version: +class MyProto(Protocol): + x: int | None = None +``` + ## unannotated-return This error is raised when a function is missing a return type annotation. This helps enforce fully-typed codebases by ensuring all functions declare their return types explicitly. To fix it, add a return type annotation to the function.