[syntax-errors] Assignment expressions before Python 3.8 (#16383)

## Summary
This PR is the first in a series derived from
https://github.com/astral-sh/ruff/pull/16308, each of which add support
for detecting one version-related syntax error from
https://github.com/astral-sh/ruff/issues/6591. This one should be
the largest because it also includes the addition of the 
`Parser::add_unsupported_syntax_error` method

Otherwise I think the general structure will be the same for each syntax
error:
* Detecting the error in the parser
* Inline parser tests for the new error
* New ruff CLI tests for the new error

## Test Plan
As noted above, there are new inline parser tests, as well as new ruff
CLI
tests. Once https://github.com/astral-sh/ruff/pull/16379 is resolved,
there should also be new mdtests for red-knot,
but this PR does not currently include those.
This commit is contained in:
Brent Westbrook 2025-02-28 17:13:46 -05:00 committed by GitHub
parent ba44e9de13
commit 4431978262
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 182 additions and 28 deletions

View file

@ -2628,6 +2628,45 @@ class A(Generic[T]):
); );
} }
#[test]
fn walrus_before_py38() {
// ok
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--stdin-filename", "test.py"])
.arg("--target-version=py38")
.arg("-")
.pass_stdin(r#"(x := 1)"#),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"
);
// not ok on 3.7 with preview
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--stdin-filename", "test.py"])
.arg("--target-version=py37")
.arg("--preview")
.arg("-")
.pass_stdin(r#"(x := 1)"#),
@r"
success: false
exit_code: 1
----- stdout -----
test.py:1:2: SyntaxError: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8)
Found 1 error.
----- stderr -----
"
);
}
#[test] #[test]
fn match_before_py310() { fn match_before_py310() {
// ok on 3.10 // ok on 3.10

View file

@ -0,0 +1,2 @@
# parse_options: { "target-version": "3.7" }
(x := 1)

View file

@ -0,0 +1,2 @@
# parse_options: { "target-version": "3.8" }
(x := 1)

View file

@ -444,31 +444,35 @@ pub struct UnsupportedSyntaxError {
pub target_version: PythonVersion, pub target_version: PythonVersion,
} }
impl UnsupportedSyntaxError { #[derive(Debug, PartialEq, Clone, Copy)]
/// The earliest allowed version for the syntax associated with this error. pub enum UnsupportedSyntaxErrorKind {
pub const fn minimum_version(&self) -> PythonVersion { Match,
match self.kind { Walrus,
UnsupportedSyntaxErrorKind::MatchBeforePy310 => PythonVersion::PY310,
}
}
} }
impl Display for UnsupportedSyntaxError { impl Display for UnsupportedSyntaxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind { let kind = match self.kind {
UnsupportedSyntaxErrorKind::MatchBeforePy310 => write!( UnsupportedSyntaxErrorKind::Match => "`match` statement",
f, UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)",
"Cannot use `match` statement on Python {} (syntax was added in Python {})", };
self.target_version, write!(
self.minimum_version(), f,
), "Cannot use {kind} on Python {} (syntax was added in Python {})",
} self.target_version,
self.minimum_version(),
)
} }
} }
#[derive(Debug, PartialEq, Clone, Copy)] impl UnsupportedSyntaxError {
pub enum UnsupportedSyntaxErrorKind { /// The earliest allowed version for the syntax associated with this error.
MatchBeforePy310, pub const fn minimum_version(&self) -> PythonVersion {
match self.kind {
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
}
}
} }
#[cfg(target_pointer_width = "64")] #[cfg(target_pointer_width = "64")]

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, StringFlags, UnaryOp, IpyEscapeKind, Number, Operator, PythonVersion, StringFlags, UnaryOp,
}; };
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@ -16,7 +16,7 @@ use crate::parser::{helpers, FunctionKind, Parser};
use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType}; use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType};
use crate::token::{TokenKind, TokenValue}; use crate::token::{TokenKind, TokenValue};
use crate::token_set::TokenSet; use crate::token_set::TokenSet;
use crate::{FStringErrorType, Mode, ParseErrorType}; use crate::{FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxErrorKind};
use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; use super::{FStringElementsKind, Parenthesized, RecoveryContextKind};
@ -2161,10 +2161,24 @@ impl<'src> Parser<'src> {
let value = self.parse_conditional_expression_or_higher(); let value = self.parse_conditional_expression_or_higher();
let range = self.node_range(start);
// test_err walrus_py37
// # parse_options: { "target-version": "3.7" }
// (x := 1)
// test_ok walrus_py38
// # parse_options: { "target-version": "3.8" }
// (x := 1)
if self.options.target_version < PythonVersion::PY38 {
self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Walrus, range);
}
ast::ExprNamed { ast::ExprNamed {
target: Box::new(target), target: Box::new(target),
value: Box::new(value.expr), value: Box::new(value.expr),
range: self.node_range(start), range,
} }
} }

View file

@ -11,7 +11,7 @@ use crate::parser::progress::{ParserProgress, TokenId};
use crate::token::TokenValue; use crate::token::TokenValue;
use crate::token_set::TokenSet; use crate::token_set::TokenSet;
use crate::token_source::{TokenSource, TokenSourceCheckpoint}; use crate::token_source::{TokenSource, TokenSourceCheckpoint};
use crate::{Mode, ParseError, ParseErrorType, TokenKind}; use crate::{Mode, ParseError, ParseErrorType, TokenKind, UnsupportedSyntaxErrorKind};
use crate::{Parsed, Tokens}; use crate::{Parsed, Tokens};
pub use crate::parser::options::ParseOptions; pub use crate::parser::options::ParseOptions;
@ -438,6 +438,16 @@ impl<'src> Parser<'src> {
inner(&mut self.errors, error, ranged.range()); inner(&mut self.errors, error, ranged.range());
} }
/// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and
/// [`TextRange`].
fn add_unsupported_syntax_error(&mut self, kind: UnsupportedSyntaxErrorKind, range: TextRange) {
self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
kind,
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.
fn at(&self, kind: TokenKind) -> bool { fn at(&self, kind: TokenKind) -> bool {
self.current_token_kind() == kind self.current_token_kind() == kind

View file

@ -17,7 +17,7 @@ use crate::parser::{
}; };
use crate::token::{TokenKind, TokenValue}; use crate::token::{TokenKind, TokenValue};
use crate::token_set::TokenSet; use crate::token_set::TokenSet;
use crate::{Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind}; use crate::{Mode, ParseErrorType, UnsupportedSyntaxErrorKind};
use super::expression::ExpressionContext; use super::expression::ExpressionContext;
use super::Parenthesized; use super::Parenthesized;
@ -2278,11 +2278,7 @@ impl<'src> Parser<'src> {
// pass // pass
if self.options.target_version < PythonVersion::PY310 { if self.options.target_version < PythonVersion::PY310 {
self.unsupported_syntax_errors.push(UnsupportedSyntaxError { self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Match, match_range);
kind: UnsupportedSyntaxErrorKind::MatchBeforePy310,
range: match_range,
target_version: self.options.target_version,
});
} }
ast::StmtMatch { ast::StmtMatch {

View file

@ -0,0 +1,47 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/walrus_py37.py
---
## AST
```
Module(
ModModule {
range: 0..54,
body: [
Expr(
StmtExpr {
range: 45..53,
value: Named(
ExprNamed {
range: 46..52,
target: Name(
ExprName {
range: 46..47,
id: Name("x"),
ctx: Store,
},
),
value: NumberLiteral(
ExprNumberLiteral {
range: 51..52,
value: Int(
1,
),
},
),
},
),
},
),
],
},
)
```
## Unsupported Syntax Errors
|
1 | # parse_options: { "target-version": "3.7" }
2 | (x := 1)
| ^^^^^^ Syntax Error: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8)
|

View file

@ -0,0 +1,40 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/walrus_py38.py
---
## AST
```
Module(
ModModule {
range: 0..54,
body: [
Expr(
StmtExpr {
range: 45..53,
value: Named(
ExprNamed {
range: 46..52,
target: Name(
ExprName {
range: 46..47,
id: Name("x"),
ctx: Store,
},
),
value: NumberLiteral(
ExprNumberLiteral {
range: 51..52,
value: Int(
1,
),
},
),
},
),
},
),
],
},
)
```