Skip to content

Commit

Permalink
[syntax-errors] Assignment expressions before Python 3.8 (#16383)
Browse files Browse the repository at this point in the history
## Summary
This PR is the first in a series derived from
#16308, each of which add support
for detecting one version-related syntax error from
#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 #16379 is resolved,
there should also be new mdtests for red-knot,
but this PR does not currently include those.
  • Loading branch information
ntBre authored Feb 28, 2025
1 parent ba44e9d commit 4431978
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 28 deletions.
39 changes: 39 additions & 0 deletions crates/ruff/tests/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_python_parser/resources/inline/err/walrus_py37.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# parse_options: { "target-version": "3.7" }
(x := 1)
2 changes: 2 additions & 0 deletions crates/ruff_python_parser/resources/inline/ok/walrus_py38.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# parse_options: { "target-version": "3.8" }
(x := 1)
40 changes: 22 additions & 18 deletions crates/ruff_python_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
20 changes: 17 additions & 3 deletions crates/ruff_python_parser/src/parser/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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};

Expand Down Expand Up @@ -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,
}
}

Expand Down
12 changes: 11 additions & 1 deletion crates/ruff_python_parser/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions crates/ruff_python_parser/src/parser/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
|
Original file line number Diff line number Diff line change
@@ -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,
),
},
),
},
),
},
),
],
},
)
```

0 comments on commit 4431978

Please sign in to comment.