[syntax-errors] except* before Python 3.11 (#16446)

Summary
--

One of the simpler ones, just detect the use of `except*` before 3.11.

Test Plan
--

New inline tests.
This commit is contained in:
Brent Westbrook 2025-03-02 13:20:18 -05:00 committed by GitHub
parent 0d615b8765
commit e924ecbdac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 180 additions and 23 deletions

View file

@ -613,7 +613,7 @@ mod tests {
let settings = Settings { let settings = Settings {
cache_dir, cache_dir,
linter: LinterSettings { linter: LinterSettings {
unresolved_target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::latest(),
..Default::default() ..Default::default()
}, },
..Settings::default() ..Settings::default()

View file

@ -0,0 +1,3 @@
# parse_options: {"target-version": "3.10"}
try: ...
except* ValueError: ...

View file

@ -0,0 +1,3 @@
# parse_options: {"target-version": "3.11"}
try: ...
except* ValueError: ...

View file

@ -438,9 +438,9 @@ pub struct UnsupportedSyntaxError {
/// The target [`PythonVersion`] for which this error was detected. /// The target [`PythonVersion`] for which this error was detected.
/// ///
/// This is different from the version reported by the /// This is different from the version reported by the
/// [`minimum_version`](UnsupportedSyntaxError::minimum_version) method, which is the earliest /// [`minimum_version`](UnsupportedSyntaxErrorKind::minimum_version) method, which is the
/// allowed version for this piece of syntax. The `target_version` is primarily used for /// earliest allowed version for this piece of syntax. The `target_version` is primarily used
/// user-facing error messages. /// for user-facing error messages.
pub target_version: PythonVersion, pub target_version: PythonVersion,
} }
@ -448,6 +448,7 @@ pub struct UnsupportedSyntaxError {
pub enum UnsupportedSyntaxErrorKind { pub enum UnsupportedSyntaxErrorKind {
Match, Match,
Walrus, Walrus,
ExceptStar,
} }
impl Display for UnsupportedSyntaxError { impl Display for UnsupportedSyntaxError {
@ -455,22 +456,24 @@ impl Display for UnsupportedSyntaxError {
let kind = match self.kind { let kind = match self.kind {
UnsupportedSyntaxErrorKind::Match => "`match` statement", UnsupportedSyntaxErrorKind::Match => "`match` statement",
UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)", UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)",
UnsupportedSyntaxErrorKind::ExceptStar => "`except*`",
}; };
write!( write!(
f, f,
"Cannot use {kind} on Python {} (syntax was added in Python {})", "Cannot use {kind} on Python {} (syntax was added in Python {})",
self.target_version, self.target_version,
self.minimum_version(), self.kind.minimum_version(),
) )
} }
} }
impl UnsupportedSyntaxError { impl UnsupportedSyntaxErrorKind {
/// The earliest allowed version for the syntax associated with this error. /// The earliest allowed version for the syntax associated with this error.
pub const fn minimum_version(&self) -> PythonVersion { pub const fn minimum_version(&self) -> PythonVersion {
match self.kind { match self {
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311,
} }
} }
} }

View file

@ -7,7 +7,7 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements, self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements,
IpyEscapeKind, Number, Operator, PythonVersion, StringFlags, UnaryOp, IpyEscapeKind, Number, Operator, StringFlags, UnaryOp,
}; };
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@ -2171,9 +2171,7 @@ impl<'src> Parser<'src> {
// # parse_options: { "target-version": "3.8" } // # parse_options: { "target-version": "3.8" }
// (x := 1) // (x := 1)
if self.options.target_version < PythonVersion::PY38 { self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Walrus, range);
self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Walrus, range);
}
ast::ExprNamed { ast::ExprNamed {
target: Box::new(target), target: Box::new(target),

View file

@ -439,13 +439,15 @@ impl<'src> Parser<'src> {
} }
/// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and /// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and
/// [`TextRange`]. /// [`TextRange`] if its minimum version is less than [`Parser::target_version`].
fn add_unsupported_syntax_error(&mut self, kind: UnsupportedSyntaxErrorKind, range: TextRange) { fn add_unsupported_syntax_error(&mut self, kind: UnsupportedSyntaxErrorKind, range: TextRange) {
self.unsupported_syntax_errors.push(UnsupportedSyntaxError { if self.options.target_version < kind.minimum_version() {
kind, self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
range, kind,
target_version: self.options.target_version, range,
}); target_version: self.options.target_version,
});
}
} }
/// Returns `true` if the current token is of the given kind. /// Returns `true` if the current token is of the given kind.

View file

@ -5,8 +5,7 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt, self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, Stmt, WithItem,
WithItem,
}; };
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};
@ -1458,13 +1457,28 @@ impl<'src> Parser<'src> {
); );
} }
// test_ok except_star_py311
// # parse_options: {"target-version": "3.11"}
// try: ...
// except* ValueError: ...
// test_err except_star_py310
// # parse_options: {"target-version": "3.10"}
// try: ...
// except* ValueError: ...
let range = self.node_range(try_start);
if is_star {
self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::ExceptStar, range);
}
ast::StmtTry { ast::StmtTry {
body: try_body, body: try_body,
handlers, handlers,
orelse, orelse,
finalbody, finalbody,
is_star, is_star,
range: self.node_range(try_start), range,
} }
} }
@ -2277,9 +2291,7 @@ impl<'src> Parser<'src> {
// case 1: // case 1:
// pass // pass
if self.options.target_version < PythonVersion::PY310 { self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Match, match_range);
self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Match, match_range);
}
ast::StmtMatch { ast::StmtMatch {
subject: Box::new(subject), subject: Box::new(subject),

View file

@ -0,0 +1,72 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/except_star_py310.py
---
## AST
```
Module(
ModModule {
range: 0..77,
body: [
Try(
StmtTry {
range: 44..76,
body: [
Expr(
StmtExpr {
range: 49..52,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 49..52,
},
),
},
),
],
handlers: [
ExceptHandler(
ExceptHandlerExceptHandler {
range: 53..76,
type_: Some(
Name(
ExprName {
range: 61..71,
id: Name("ValueError"),
ctx: Load,
},
),
),
name: None,
body: [
Expr(
StmtExpr {
range: 73..76,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 73..76,
},
),
},
),
],
},
),
],
orelse: [],
finalbody: [],
is_star: true,
},
),
],
},
)
```
## Unsupported Syntax Errors
|
1 | # parse_options: {"target-version": "3.10"}
2 | / try: ...
3 | | except* ValueError: ...
| |_______________________^ Syntax Error: Cannot use `except*` on Python 3.10 (syntax was added in Python 3.11)
|

View file

@ -0,0 +1,64 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/except_star_py311.py
---
## AST
```
Module(
ModModule {
range: 0..77,
body: [
Try(
StmtTry {
range: 44..76,
body: [
Expr(
StmtExpr {
range: 49..52,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 49..52,
},
),
},
),
],
handlers: [
ExceptHandler(
ExceptHandlerExceptHandler {
range: 53..76,
type_: Some(
Name(
ExprName {
range: 61..71,
id: Name("ValueError"),
ctx: Load,
},
),
),
name: None,
body: [
Expr(
StmtExpr {
range: 73..76,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 73..76,
},
),
},
),
],
},
),
],
orelse: [],
finalbody: [],
is_star: true,
},
),
],
},
)
```