diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index 31589aa3a8..0ec437e502 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -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] fn match_before_py310() { // ok on 3.10 diff --git a/crates/ruff_python_parser/resources/inline/err/walrus_py37.py b/crates/ruff_python_parser/resources/inline/err/walrus_py37.py new file mode 100644 index 0000000000..a577f344bd --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/walrus_py37.py @@ -0,0 +1,2 @@ +# parse_options: { "target-version": "3.7" } +(x := 1) diff --git a/crates/ruff_python_parser/resources/inline/ok/walrus_py38.py b/crates/ruff_python_parser/resources/inline/ok/walrus_py38.py new file mode 100644 index 0000000000..819b99dec7 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/walrus_py38.py @@ -0,0 +1,2 @@ +# parse_options: { "target-version": "3.8" } +(x := 1) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 0c5435e91f..9b21752442 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -444,31 +444,35 @@ pub struct UnsupportedSyntaxError { pub target_version: PythonVersion, } -impl UnsupportedSyntaxError { - /// The earliest allowed version for the syntax associated with this error. - pub const fn minimum_version(&self) -> PythonVersion { - match self.kind { - UnsupportedSyntaxErrorKind::MatchBeforePy310 => PythonVersion::PY310, - } - } +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum UnsupportedSyntaxErrorKind { + Match, + Walrus, } impl Display for UnsupportedSyntaxError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.kind { - UnsupportedSyntaxErrorKind::MatchBeforePy310 => write!( - f, - "Cannot use `match` statement on Python {} (syntax was added in Python {})", - self.target_version, - self.minimum_version(), - ), - } + let kind = match self.kind { + UnsupportedSyntaxErrorKind::Match => "`match` statement", + UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)", + }; + write!( + f, + "Cannot use {kind} on Python {} (syntax was added in Python {})", + self.target_version, + self.minimum_version(), + ) } } -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum UnsupportedSyntaxErrorKind { - MatchBeforePy310, +impl UnsupportedSyntaxError { + /// The earliest allowed version for the syntax associated with this error. + pub const fn minimum_version(&self) -> PythonVersion { + match self.kind { + UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, + UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, + } + } } #[cfg(target_pointer_width = "64")] diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 31d8b363d4..2a0db69997 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -7,7 +7,7 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_python_ast::name::Name; use ruff_python_ast::{ 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}; @@ -16,7 +16,7 @@ use crate::parser::{helpers, FunctionKind, Parser}; use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; -use crate::{FStringErrorType, Mode, ParseErrorType}; +use crate::{FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxErrorKind}; use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; @@ -2161,10 +2161,24 @@ impl<'src> Parser<'src> { 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 { target: Box::new(target), value: Box::new(value.expr), - range: self.node_range(start), + range, } } diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index e7382f6e0b..81ac52d973 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -11,7 +11,7 @@ use crate::parser::progress::{ParserProgress, TokenId}; use crate::token::TokenValue; use crate::token_set::TokenSet; use crate::token_source::{TokenSource, TokenSourceCheckpoint}; -use crate::{Mode, ParseError, ParseErrorType, TokenKind}; +use crate::{Mode, ParseError, ParseErrorType, TokenKind, UnsupportedSyntaxErrorKind}; use crate::{Parsed, Tokens}; pub use crate::parser::options::ParseOptions; @@ -438,6 +438,16 @@ impl<'src> Parser<'src> { 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. fn at(&self, kind: TokenKind) -> bool { self.current_token_kind() == kind diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 24bed309a3..64bc40fee4 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -17,7 +17,7 @@ use crate::parser::{ }; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; -use crate::{Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind}; +use crate::{Mode, ParseErrorType, UnsupportedSyntaxErrorKind}; use super::expression::ExpressionContext; use super::Parenthesized; @@ -2278,11 +2278,7 @@ impl<'src> Parser<'src> { // pass if self.options.target_version < PythonVersion::PY310 { - self.unsupported_syntax_errors.push(UnsupportedSyntaxError { - kind: UnsupportedSyntaxErrorKind::MatchBeforePy310, - range: match_range, - target_version: self.options.target_version, - }); + self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Match, match_range); } ast::StmtMatch { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap new file mode 100644 index 0000000000..402445a7f6 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap @@ -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) + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@walrus_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@walrus_py38.py.snap new file mode 100644 index 0000000000..b0dfb2c4db --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@walrus_py38.py.snap @@ -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, + ), + }, + ), + }, + ), + }, + ), + ], + }, +) +```