Skip to content

Commit 5697d21

Browse files
authored
[syntax-errors] Irrefutable case pattern before final case (#16905)
Summary -- Detects irrefutable `match` cases before the final case using a modified version of the existing `Pattern::is_irrefutable` method from the AST crate. The modified method helps to retrieve a more precise diagnostic range to match what Python 3.13 shows in the REPL. Test Plan -- New inline tests, as well as some updates to existing tests that had irrefutable patterns before the last block.
1 parent 58350ec commit 5697d21

17 files changed

+1354
-528
lines changed

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,12 +549,14 @@ impl SemanticSyntaxContext for Checker<'_> {
549549
}
550550
SemanticSyntaxErrorKind::ReboundComprehensionVariable
551551
| SemanticSyntaxErrorKind::DuplicateTypeParameter
552+
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_)
552553
if self.settings.preview.is_enabled() =>
553554
{
554555
self.semantic_errors.borrow_mut().push(error);
555556
}
556557
SemanticSyntaxErrorKind::ReboundComprehensionVariable
557-
| SemanticSyntaxErrorKind::DuplicateTypeParameter => {}
558+
| SemanticSyntaxErrorKind::DuplicateTypeParameter
559+
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_) => {}
558560
}
559561
}
560562
}

crates/ruff_python_ast/src/nodes.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,12 +2244,33 @@ impl Pattern {
22442244
///
22452245
/// [irrefutable pattern]: https://peps.python.org/pep-0634/#irrefutable-case-blocks
22462246
pub fn is_irrefutable(&self) -> bool {
2247+
self.irrefutable_pattern().is_some()
2248+
}
2249+
2250+
/// Return `Some(IrrefutablePattern)` if `self` is irrefutable or `None` otherwise.
2251+
pub fn irrefutable_pattern(&self) -> Option<IrrefutablePattern> {
22472252
match self {
2248-
Pattern::MatchAs(PatternMatchAs { pattern: None, .. }) => true,
2253+
Pattern::MatchAs(PatternMatchAs {
2254+
pattern,
2255+
name,
2256+
range,
2257+
}) => match pattern {
2258+
Some(pattern) => pattern.irrefutable_pattern(),
2259+
None => match name {
2260+
Some(name) => Some(IrrefutablePattern {
2261+
kind: IrrefutablePatternKind::Name(name.id.clone()),
2262+
range: *range,
2263+
}),
2264+
None => Some(IrrefutablePattern {
2265+
kind: IrrefutablePatternKind::Wildcard,
2266+
range: *range,
2267+
}),
2268+
},
2269+
},
22492270
Pattern::MatchOr(PatternMatchOr { patterns, .. }) => {
2250-
patterns.iter().any(Pattern::is_irrefutable)
2271+
patterns.iter().find_map(Pattern::irrefutable_pattern)
22512272
}
2252-
_ => false,
2273+
_ => None,
22532274
}
22542275
}
22552276

@@ -2277,6 +2298,17 @@ impl Pattern {
22772298
}
22782299
}
22792300

2301+
pub struct IrrefutablePattern {
2302+
pub kind: IrrefutablePatternKind,
2303+
pub range: TextRange,
2304+
}
2305+
2306+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2307+
pub enum IrrefutablePatternKind {
2308+
Name(Name),
2309+
Wildcard,
2310+
}
2311+
22802312
/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)
22812313
#[derive(Clone, Debug, PartialEq)]
22822314
pub struct PatternMatchValue {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
match x:
2+
case var: ... # capture pattern
3+
case 2: ...
4+
match x:
5+
case _: ...
6+
case 2: ... # wildcard pattern
7+
match x:
8+
case var1 as var2: ... # as pattern with irrefutable left-hand side
9+
case 2: ...
10+
match x:
11+
case enum.variant | var: ... # or pattern with irrefutable part
12+
case 2: ...
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
match x:
2+
case 2: ...
3+
case var: ...
4+
match x:
5+
case 2: ...
6+
case _: ...
7+
match x:
8+
case var if True: ... # don't try to refute a guarded pattern
9+
case 2: ...
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
match foo:
22
case foo_bar: ...
3+
match foo:
34
case _: ...
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
match foo:
22
case case: ...
3+
match foo:
34
case match: ...
5+
match foo:
46
case type: ...
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
match subject:
2-
case a: ...
32
case a if x: ...
43
case a, b: ...
54
case a, b if x: ...
5+
case a: ...

crates/ruff_python_parser/resources/valid/statement/match.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,18 +243,21 @@
243243
match x:
244244
case a:
245245
...
246+
match x:
246247
case a as b:
247248
...
249+
match x:
248250
case 1 | 2 as two:
249251
...
250252
case 1 + 3j as sum:
251253
...
252254
case a.b as ab:
253255
...
254-
case _:
255-
...
256256
case _ as x:
257257
...
258+
match x:
259+
case _:
260+
...
258261

259262
# PatternMatchSequence
260263
match x:

crates/ruff_python_parser/src/parser/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,10 +1060,10 @@ impl RecoveryContextKind {
10601060
None => {
10611061
// test_ok match_sequence_pattern_terminator
10621062
// match subject:
1063-
// case a: ...
10641063
// case a if x: ...
10651064
// case a, b: ...
10661065
// case a, b if x: ...
1066+
// case a: ...
10671067
matches!(p.current_token_kind(), TokenKind::Colon | TokenKind::If)
10681068
.then_some(ListTerminatorKind::Regular)
10691069
}

crates/ruff_python_parser/src/parser/pattern.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,13 +488,16 @@ impl Parser<'_> {
488488
// test_ok match_as_pattern_soft_keyword
489489
// match foo:
490490
// case case: ...
491+
// match foo:
491492
// case match: ...
493+
// match foo:
492494
// case type: ...
493495
let ident = self.parse_identifier();
494496

495497
// test_ok match_as_pattern
496498
// match foo:
497499
// case foo_bar: ...
500+
// match foo:
498501
// case _: ...
499502
Pattern::MatchAs(ast::PatternMatchAs {
500503
range: ident.range,

crates/ruff_python_parser/src/semantic_errors.rs

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::fmt::Display;
99
use ruff_python_ast::{
1010
self as ast,
1111
visitor::{walk_expr, Visitor},
12-
Expr, PythonVersion, Stmt, StmtExpr, StmtImportFrom,
12+
Expr, IrrefutablePatternKind, PythonVersion, Stmt, StmtExpr, StmtImportFrom,
1313
};
1414
use ruff_text_size::{Ranged, TextRange};
1515

@@ -54,27 +54,30 @@ impl SemanticSyntaxChecker {
5454
}
5555

5656
fn check_stmt<Ctx: SemanticSyntaxContext>(&mut self, stmt: &ast::Stmt, ctx: &Ctx) {
57-
if let Stmt::ImportFrom(StmtImportFrom { range, module, .. }) = stmt {
58-
if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) {
59-
Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range);
57+
match stmt {
58+
Stmt::ImportFrom(StmtImportFrom { range, module, .. }) => {
59+
if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) {
60+
Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range);
61+
}
62+
}
63+
Stmt::Match(match_stmt) => {
64+
Self::irrefutable_match_case(match_stmt, ctx);
65+
}
66+
Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. })
67+
| Stmt::ClassDef(ast::StmtClassDef { type_params, .. })
68+
| Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => {
69+
if let Some(type_params) = type_params {
70+
Self::duplicate_type_parameter_name(type_params, ctx);
71+
}
6072
}
73+
_ => {}
6174
}
62-
63-
Self::duplicate_type_parameter_name(stmt, ctx);
6475
}
6576

66-
fn duplicate_type_parameter_name<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
67-
let (Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. })
68-
| Stmt::ClassDef(ast::StmtClassDef { type_params, .. })
69-
| Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. })) = stmt
70-
else {
71-
return;
72-
};
73-
74-
let Some(type_params) = type_params else {
75-
return;
76-
};
77-
77+
fn duplicate_type_parameter_name<Ctx: SemanticSyntaxContext>(
78+
type_params: &ast::TypeParams,
79+
ctx: &Ctx,
80+
) {
7881
if type_params.len() < 2 {
7982
return;
8083
}
@@ -109,6 +112,49 @@ impl SemanticSyntaxChecker {
109112
}
110113
}
111114

115+
fn irrefutable_match_case<Ctx: SemanticSyntaxContext>(stmt: &ast::StmtMatch, ctx: &Ctx) {
116+
// test_ok irrefutable_case_pattern_at_end
117+
// match x:
118+
// case 2: ...
119+
// case var: ...
120+
// match x:
121+
// case 2: ...
122+
// case _: ...
123+
// match x:
124+
// case var if True: ... # don't try to refute a guarded pattern
125+
// case 2: ...
126+
127+
// test_err irrefutable_case_pattern
128+
// match x:
129+
// case var: ... # capture pattern
130+
// case 2: ...
131+
// match x:
132+
// case _: ...
133+
// case 2: ... # wildcard pattern
134+
// match x:
135+
// case var1 as var2: ... # as pattern with irrefutable left-hand side
136+
// case 2: ...
137+
// match x:
138+
// case enum.variant | var: ... # or pattern with irrefutable part
139+
// case 2: ...
140+
for case in stmt
141+
.cases
142+
.iter()
143+
.rev()
144+
.skip(1)
145+
.filter_map(|case| match case.guard {
146+
Some(_) => None,
147+
None => case.pattern.irrefutable_pattern(),
148+
})
149+
{
150+
Self::add_error(
151+
ctx,
152+
SemanticSyntaxErrorKind::IrrefutableCasePattern(case.kind),
153+
case.range,
154+
);
155+
}
156+
}
157+
112158
pub fn visit_stmt<Ctx: SemanticSyntaxContext>(&mut self, stmt: &ast::Stmt, ctx: &Ctx) {
113159
// update internal state
114160
match stmt {
@@ -209,7 +255,7 @@ pub struct SemanticSyntaxError {
209255

210256
impl Display for SemanticSyntaxError {
211257
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212-
match self.kind {
258+
match &self.kind {
213259
SemanticSyntaxErrorKind::LateFutureImport => {
214260
f.write_str("__future__ imports must be at the top of the file")
215261
}
@@ -219,11 +265,23 @@ impl Display for SemanticSyntaxError {
219265
SemanticSyntaxErrorKind::DuplicateTypeParameter => {
220266
f.write_str("duplicate type parameter")
221267
}
268+
SemanticSyntaxErrorKind::IrrefutableCasePattern(kind) => match kind {
269+
// These error messages are taken from CPython's syntax errors
270+
IrrefutablePatternKind::Name(name) => {
271+
write!(
272+
f,
273+
"name capture `{name}` makes remaining patterns unreachable"
274+
)
275+
}
276+
IrrefutablePatternKind::Wildcard => {
277+
f.write_str("wildcard makes remaining patterns unreachable")
278+
}
279+
},
222280
}
223281
}
224282
}
225283

226-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
227285
pub enum SemanticSyntaxErrorKind {
228286
/// Represents the use of a `__future__` import after the beginning of a file.
229287
///
@@ -265,6 +323,26 @@ pub enum SemanticSyntaxErrorKind {
265323
/// class C[T, T]: ...
266324
/// ```
267325
DuplicateTypeParameter,
326+
327+
/// Represents an irrefutable `case` pattern before the last `case` in a `match` statement.
328+
///
329+
/// According to the [Python reference], "a match statement may have at most one irrefutable
330+
/// case block, and it must be last."
331+
///
332+
/// ## Examples
333+
///
334+
/// ```python
335+
/// match x:
336+
/// case value: ... # irrefutable capture pattern
337+
/// case other: ...
338+
///
339+
/// match x:
340+
/// case _: ... # irrefutable wildcard pattern
341+
/// case other: ...
342+
/// ```
343+
///
344+
/// [Python reference]: https://docs.python.org/3/reference/compound_stmts.html#irrefutable-case-blocks
345+
IrrefutableCasePattern(IrrefutablePatternKind),
268346
}
269347

270348
/// Searches for the first named expression (`x := y`) rebinding one of the `iteration_variables` in

0 commit comments

Comments
 (0)