Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# parse_options: {"target-version": "3.8"}
Comment thread
ntBre marked this conversation as resolved.
f((a)=1)
f((a) = 1)
f( ( a ) = 1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# parse_options: {"target-version": "3.7"}
f((a)=1)
89 changes: 71 additions & 18 deletions crates/ruff_python_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,11 +436,6 @@ pub struct UnsupportedSyntaxError {
pub kind: UnsupportedSyntaxErrorKind,
pub range: TextRange,
/// The target [`PythonVersion`] for which this error was detected.
///
/// This is different from the version reported by the
/// [`minimum_version`](UnsupportedSyntaxErrorKind::minimum_version) method, which is the
/// earliest allowed version for this piece of syntax. The `target_version` is primarily used
/// for user-facing error messages.
pub target_version: PythonVersion,
}

Expand All @@ -457,6 +452,26 @@ pub enum UnsupportedSyntaxErrorKind {
Walrus,
ExceptStar,

/// Represents the use of a parenthesized keyword argument name after Python 3.8.
///
/// ## Example
///
/// From [BPO 34641] it sounds like this was only accidentally supported and was removed when
/// noticed. Code like this used to be valid:
///
/// ```python
/// f((a)=1)
/// ```
///
/// After Python 3.8, you have to omit the parentheses around `a`:
///
/// ```python
/// f(a=1)
/// ```
///
/// [BPO 34641]: https://github.com/python/cpython/issues/78822
ParenthesizedKeywordArgumentName,

/// Represents the use of unparenthesized tuple unpacking in a `return` statement or `yield`
/// expression before Python 3.8.
///
Expand Down Expand Up @@ -603,6 +618,9 @@ impl Display for UnsupportedSyntaxError {
UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement",
UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)",
UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`",
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => {
"Cannot use parenthesized keyword argument name"
}
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return) => {
"Cannot use iterable unpacking in return statements"
}
Expand All @@ -619,30 +637,65 @@ impl Display for UnsupportedSyntaxError {
"Cannot set default type for a type parameter"
}
};

write!(
f,
"{kind} on Python {} (syntax was added in Python {})",
"{kind} on Python {} (syntax was {changed})",
self.target_version,
self.kind.minimum_version(),
changed = self.kind.changed_version(),
)
}
}

/// Represents the kind of change in Python syntax between versions.
enum Change {
Added(PythonVersion),
Removed(PythonVersion),
}

impl Display for Change {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Change::Added(version) => write!(f, "added in Python {version}"),
Change::Removed(version) => write!(f, "removed in Python {version}"),
}
}
}

impl UnsupportedSyntaxErrorKind {
/// The earliest allowed version for the syntax associated with this error.
pub const fn minimum_version(&self) -> PythonVersion {
/// Returns the Python version when the syntax associated with this error was changed, and the
/// type of [`Change`] (added or removed).
const fn changed_version(self) -> Change {
match self {
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311,
UnsupportedSyntaxErrorKind::StarTuple(_) => PythonVersion::PY38,
UnsupportedSyntaxErrorKind::RelaxedDecorator => PythonVersion::PY39,
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => PythonVersion::PY38,
UnsupportedSyntaxErrorKind::TypeParameterList => PythonVersion::PY312,
UnsupportedSyntaxErrorKind::TypeAliasStatement => PythonVersion::PY312,
UnsupportedSyntaxErrorKind::TypeParamDefault => PythonVersion::PY313,
UnsupportedSyntaxErrorKind::Match => Change::Added(PythonVersion::PY310),
UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311),
UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::RelaxedDecorator => Change::Added(PythonVersion::PY39),
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
Change::Added(PythonVersion::PY38)
}
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => {
Change::Removed(PythonVersion::PY38)
}
UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313),
}
}

/// Returns whether or not this kind of syntax is unsupported on `target_version`.
pub(crate) fn is_unsupported(self, target_version: PythonVersion) -> bool {
match self.changed_version() {
Change::Added(version) => target_version < version,
Change::Removed(version) => target_version >= version,
}
}

/// Returns `true` if this kind of syntax is supported on `target_version`.
pub(crate) fn is_supported(self, target_version: PythonVersion) -> bool {
!self.is_unsupported(target_version)
}
}

#[cfg(target_pointer_width = "64")]
Expand Down
24 changes: 23 additions & 1 deletion crates/ruff_python_parser/src/parser/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -702,9 +702,31 @@ impl<'src> Parser<'src> {
}
}

let arg_range = parser.node_range(start);
if parser.eat(TokenKind::Equal) {
seen_keyword_argument = true;
let arg = if let Expr::Name(ident_expr) = parsed_expr.expr {
let arg = if let ParsedExpr {
expr: Expr::Name(ident_expr),
is_parenthesized,
} = parsed_expr
{
// test_ok parenthesized_kwarg_py37
// # parse_options: {"target-version": "3.7"}
// f((a)=1)

// test_err parenthesized_kwarg_py38
// # parse_options: {"target-version": "3.8"}
// f((a)=1)
Comment thread
dhruvmanila marked this conversation as resolved.
// f((a) = 1)
// f( ( a ) = 1)

if is_parenthesized {
parser.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName,
arg_range,
);
}

ast::Identifier {
id: ident_expr.id,
range: ident_expr.range,
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_python_parser/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ impl<'src> Parser<'src> {
/// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and
/// [`TextRange`] if its minimum version is less than [`Parser::target_version`].
fn add_unsupported_syntax_error(&mut self, kind: UnsupportedSyntaxErrorKind, range: TextRange) {
if self.options.target_version < kind.minimum_version() {
if kind.is_unsupported(self.options.target_version) {
self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
kind,
range,
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_python_parser/src/parser/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ impl<'src> Parser<'src> {
/// are only allowed in Python 3.8 and later: <https://github.com/python/cpython/issues/76298>.
pub(crate) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: StarTupleKind) {
let kind = UnsupportedSyntaxErrorKind::StarTuple(kind);
if self.options.target_version >= kind.minimum_version() {
if kind.is_supported(self.options.target_version) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py
---
## AST

```
Module(
ModModule {
range: 0..77,
body: [
Expr(
StmtExpr {
range: 43..51,
value: Call(
ExprCall {
range: 43..51,
func: Name(
ExprName {
range: 43..44,
id: Name("f"),
ctx: Load,
},
),
arguments: Arguments {
range: 44..51,
args: [],
keywords: [
Keyword {
range: 45..50,
arg: Some(
Identifier {
id: Name("a"),
range: 46..47,
},
),
value: NumberLiteral(
ExprNumberLiteral {
range: 49..50,
value: Int(
1,
),
},
),
},
],
},
},
),
},
),
Expr(
StmtExpr {
range: 52..62,
value: Call(
ExprCall {
range: 52..62,
func: Name(
ExprName {
range: 52..53,
id: Name("f"),
ctx: Load,
},
),
arguments: Arguments {
range: 53..62,
args: [],
keywords: [
Keyword {
range: 54..61,
arg: Some(
Identifier {
id: Name("a"),
range: 55..56,
},
),
value: NumberLiteral(
ExprNumberLiteral {
range: 60..61,
value: Int(
1,
),
},
),
},
],
},
},
),
},
),
Expr(
StmtExpr {
range: 63..76,
value: Call(
ExprCall {
range: 63..76,
func: Name(
ExprName {
range: 63..64,
id: Name("f"),
ctx: Load,
},
),
arguments: Arguments {
range: 64..76,
args: [],
keywords: [
Keyword {
range: 66..75,
arg: Some(
Identifier {
id: Name("a"),
range: 68..69,
},
),
value: NumberLiteral(
ExprNumberLiteral {
range: 74..75,
value: Int(
1,
),
},
),
},
],
},
},
),
},
),
],
},
)
```
## Unsupported Syntax Errors

|
1 | # parse_options: {"target-version": "3.8"}
2 | f((a)=1)
| ^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8)
3 | f((a) = 1)
4 | f( ( a ) = 1)
|


|
1 | # parse_options: {"target-version": "3.8"}
2 | f((a)=1)
3 | f((a) = 1)
| ^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8)
4 | f( ( a ) = 1)
|


|
2 | f((a)=1)
3 | f((a) = 1)
4 | f( ( a ) = 1)
| ^^^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8)
|
Loading