diff --git a/crates/pyrefly_config/src/error_kind.rs b/crates/pyrefly_config/src/error_kind.rs index 09b631896d..1df6cdb96f 100644 --- a/crates/pyrefly_config/src/error_kind.rs +++ b/crates/pyrefly_config/src/error_kind.rs @@ -174,6 +174,8 @@ pub enum ErrorKind { InvalidOverload, /// An error related to ParamSpec definition or usage. InvalidParamSpec, + /// An error caused by an invalid match pattern. + InvalidPattern, /// A use of `typing.Self` in a context where Pyrefly does not recognize it as /// mapping to a valid class type. InvalidSelfType, diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index 8ae5d86a48..f5a8eed483 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -18,6 +18,7 @@ use pyrefly_types::facet::UnresolvedFacetKind; use pyrefly_types::simplify::intersect; use pyrefly_types::type_info::JoinStyle; use pyrefly_util::prelude::SliceExt; +use pyrefly_util::visit::Visit; use ruff_python_ast::Arguments; use ruff_python_ast::AtomicNodeIndex; use ruff_python_ast::Expr; @@ -752,6 +753,25 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { let right = self.expr_infer(v, errors); self.narrow_isinstance(ty, &right) } + AtomicNarrowOp::IsInstancePattern(v) => { + let right = self.expr_infer(v, errors); + let mut contains_subscript = false; + v.visit(&mut |e| { + if matches!(e, Expr::Subscript(_)) { + contains_subscript = true; + } + }); + self.check_type_is_class_object( + right.clone(), + Some(ty.clone()), + contains_subscript, + v.range(), + &FunctionKind::IsInstance, + errors, + ErrorKind::InvalidPattern, + ); + self.narrow_isinstance(ty, &right) + } AtomicNarrowOp::IsNotInstance(v) => { let right = self.expr_infer(v, errors); self.narrow_is_not_instance(ty, &right) diff --git a/pyrefly/lib/alt/special_calls.rs b/pyrefly/lib/alt/special_calls.rs index 68f21368a4..8cb5b76bc0 100644 --- a/pyrefly/lib/alt/special_calls.rs +++ b/pyrefly/lib/alt/special_calls.rs @@ -298,7 +298,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.stdlib.bool().clone().to_type() } - fn check_type_is_class_object( + pub(crate) fn check_type_is_class_object( &self, ty: Type, object_type: Option, @@ -306,6 +306,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { range: TextRange, func_kind: &FunctionKind, errors: &ErrorCollector, + error_kind: ErrorKind, ) { for ty in self.as_class_info(ty) { if let Type::ClassDef(cls) = &ty { @@ -313,7 +314,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), "Expected class object, got `Any`".to_owned(), ); } @@ -323,7 +324,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), format!("NewType `{}` not allowed in {}", cls.name(), func_display(),), ); } @@ -332,7 +333,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), format!( "TypedDict `{}` not allowed as second argument to {}", cls.name(), @@ -346,7 +347,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), format!("Protocol `{}` is not decorated with @runtime_checkable and cannot be used with {}", cls.name(), func_display()), ); } else { @@ -358,7 +359,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), format!("Protocol `{}` has non-method members and cannot be used with issubclass()", cls.name()), ); } @@ -422,7 +423,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), format!( "Expected class object, got parameterized generic type: `{}`", self.for_display(ty) @@ -433,7 +434,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), format!("Expected class object, got special form `{}`", special_form), ); } @@ -441,7 +442,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.error( errors, range, - ErrorInfo::Kind(ErrorKind::InvalidArgument), + ErrorInfo::Kind(error_kind), format!("Expected class object, got `{}`", self.for_display(ty)), ); } else { @@ -534,6 +535,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { classinfo_expr.range(), func_kind, errors, + ErrorKind::InvalidArgument, ); } diff --git a/pyrefly/lib/binding/narrow.rs b/pyrefly/lib/binding/narrow.rs index 663ad33605..05f2d30ebe 100644 --- a/pyrefly/lib/binding/narrow.rs +++ b/pyrefly/lib/binding/narrow.rs @@ -58,6 +58,7 @@ pub enum AtomicNarrowOp { Eq(Expr), NotEq(Expr), IsInstance(Expr), + IsInstancePattern(Expr), IsNotInstance(Expr), IsSubclass(Expr), IsNotSubclass(Expr), @@ -119,6 +120,9 @@ impl DisplayWith for AtomicNarrowOp { AtomicNarrowOp::Eq(expr) => write!(f, "Eq({})", expr.display_with(ctx)), AtomicNarrowOp::NotEq(expr) => write!(f, "NotEq({})", expr.display_with(ctx)), AtomicNarrowOp::IsInstance(expr) => write!(f, "IsInstance({})", expr.display_with(ctx)), + AtomicNarrowOp::IsInstancePattern(expr) => { + write!(f, "IsInstancePattern({})", expr.display_with(ctx)) + } AtomicNarrowOp::IsNotInstance(expr) => { write!(f, "IsNotInstance({})", expr.display_with(ctx)) } @@ -220,6 +224,7 @@ impl AtomicNarrowOp { Self::Is(v) => Self::IsNot(v.clone()), Self::IsNot(v) => Self::Is(v.clone()), Self::IsInstance(v) => Self::IsNotInstance(v.clone()), + Self::IsInstancePattern(v) => Self::IsNotInstance(v.clone()), Self::IsNotInstance(v) => Self::IsInstance(v.clone()), Self::IsSubclass(v) => Self::IsNotSubclass(v.clone()), Self::IsNotSubclass(v) => Self::IsSubclass(v.clone()), @@ -875,6 +880,7 @@ impl NarrowOps { // Technically the `__class__` attribute can be mutated, but code that does that // probably isn't statically analyzable anyway. | AtomicNarrowOp::IsInstance(..) + | AtomicNarrowOp::IsInstancePattern(..) | AtomicNarrowOp::IsNotInstance(..) | AtomicNarrowOp::IsSubclass(..) | AtomicNarrowOp::IsNotSubclass(..) diff --git a/pyrefly/lib/binding/pattern.rs b/pyrefly/lib/binding/pattern.rs index d029548397..476911b10d 100644 --- a/pyrefly/lib/binding/pattern.rs +++ b/pyrefly/lib/binding/pattern.rs @@ -242,7 +242,7 @@ impl<'a> BindingsBuilder<'a> { } Pattern::MatchClass(mut x) => { self.ensure_expr(&mut x.cls, narrowing_usage); - let narrow_op = AtomicNarrowOp::IsInstance((*x.cls).clone()); + let narrow_op = AtomicNarrowOp::IsInstancePattern((*x.cls).clone()); // Redefining subject_idx to apply the class level narrowing, // which is used for additional narrowing for attributes below. let subject_idx = self.insert_binding( diff --git a/pyrefly/lib/test/pattern_match.rs b/pyrefly/lib/test/pattern_match.rs index 6470d941cd..70a1aff6eb 100644 --- a/pyrefly/lib/test/pattern_match.rs +++ b/pyrefly/lib/test/pattern_match.rs @@ -212,6 +212,21 @@ def describe_ok(color: Literal["red", "blue"]): "#, ); +testcase!( + test_enum_class_pattern_invalid, + r#" +from enum import Enum + +class Color(Enum): + RED = "red" + +def describe(color: Color) -> None: + match color: # E: Match on `Color` is not exhaustive + case Color.RED(): # E: Expected class object, got `Literal[Color.RED]` + pass +"#, +); + testcase!( test_non_exhaustive_enum_match_facet_subject, r#" diff --git a/website/docs/error-kinds.mdx b/website/docs/error-kinds.mdx index 0a7925e5c5..88abbc4189 100644 --- a/website/docs/error-kinds.mdx +++ b/website/docs/error-kinds.mdx @@ -636,6 +636,23 @@ def f(x, *args: P1.args, **kwargs: P2.kwargs) -> None: Here, `P1.args` and `P2.kwargs` can't be used together; `*args` and `**kwargs` must come from the same `ParamSpec`. +## invalid-pattern + +This error is reported when a pattern is invalid at runtime. For example, enum members are values, +so they must be matched as value patterns (without `()`), not class patterns: + +```python +from enum import Enum + +class Color(Enum): + RED = "red" + +def describe(color: Color) -> None: + match color: + case Color.RED(): # Invalid pattern: use `Color.RED` (without parentheses) + pass +``` + ## invalid-self-type This error occurs when `Self` is used in a context Pyrefly does not currently support.