From 1568ac658e7958387917d1a1933dcef9cb8d2aa9 Mon Sep 17 00:00:00 2001 From: knQzx <75641500+knQzx@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:43:25 +0200 Subject: [PATCH 1/2] flag ambiguous protocol members without type annotations add UnannotatedProtocolMember error kind that fires when a protocol class body assigns a value to a member without an explicit type annotation - matching mypy's behavior of requiring all protocol members to have declared types. --- crates/pyrefly_config/src/error_kind.rs | 2 ++ pyrefly/lib/alt/class/class_field.rs | 11 +++++++++++ pyrefly/lib/test/protocol.rs | 11 ++++++----- website/docs/error-kinds.mdx | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 5 deletions(-) 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..3936d1bfea 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -1588,6 +1588,17 @@ 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() { + 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. From a916f14881cbb92c5b9abab814edfdfee5517dea Mon Sep 17 00:00:00 2001 From: knQzx <75641500+knQzx@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:47:08 +0200 Subject: [PATCH 2/2] skip dunder attributes in unannotated protocol member check --- pyrefly/lib/alt/class/class_field.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 3936d1bfea..b74aab5899 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -1588,7 +1588,10 @@ 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() { + if metadata.is_protocol() + && direct_annotation.is_none() + && !is_dunder(name.as_str()) + { self.error( errors, range,