mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-02 09:52:32 +00:00
[syntax-errors] Parenthesized keyword argument names after Python 3.8 (#16482)
Summary -- Unlike the other syntax errors detected so far, parenthesized keyword arguments are only allowed *before* 3.8. It sounds like they were only accidentally allowed before that [^1]. As an aside, you get a pretty confusing error from Python for this, so it's nice that we can catch it: ```pycon >>> def f(**kwargs): ... ... f((a)=1) ... File "<python-input-0>", line 2 f((a)=1) ^^^ SyntaxError: expression cannot contain assignment, perhaps you meant "=="? >>> ``` Test Plan -- Inline tests. [^1]: https://github.com/python/cpython/issues/78822
This commit is contained in:
parent
6c14225c66
commit
b3c884f4f3
8 changed files with 319 additions and 22 deletions
|
@ -0,0 +1,4 @@
|
||||||
|
# parse_options: {"target-version": "3.8"}
|
||||||
|
f((a)=1)
|
||||||
|
f((a) = 1)
|
||||||
|
f( ( a ) = 1)
|
|
@ -0,0 +1,2 @@
|
||||||
|
# parse_options: {"target-version": "3.7"}
|
||||||
|
f((a)=1)
|
|
@ -436,11 +436,6 @@ pub struct UnsupportedSyntaxError {
|
||||||
pub kind: UnsupportedSyntaxErrorKind,
|
pub kind: UnsupportedSyntaxErrorKind,
|
||||||
pub range: TextRange,
|
pub range: TextRange,
|
||||||
/// 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
|
|
||||||
/// [`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,
|
pub target_version: PythonVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,6 +452,26 @@ pub enum UnsupportedSyntaxErrorKind {
|
||||||
Walrus,
|
Walrus,
|
||||||
ExceptStar,
|
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`
|
/// Represents the use of unparenthesized tuple unpacking in a `return` statement or `yield`
|
||||||
/// expression before Python 3.8.
|
/// expression before Python 3.8.
|
||||||
///
|
///
|
||||||
|
@ -603,6 +618,9 @@ impl Display for UnsupportedSyntaxError {
|
||||||
UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement",
|
UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement",
|
||||||
UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)",
|
UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)",
|
||||||
UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`",
|
UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`",
|
||||||
|
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => {
|
||||||
|
"Cannot use parenthesized keyword argument name"
|
||||||
|
}
|
||||||
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return) => {
|
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return) => {
|
||||||
"Cannot use iterable unpacking in return statements"
|
"Cannot use iterable unpacking in return statements"
|
||||||
}
|
}
|
||||||
|
@ -619,32 +637,67 @@ impl Display for UnsupportedSyntaxError {
|
||||||
"Cannot set default type for a type parameter"
|
"Cannot set default type for a type parameter"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{kind} on Python {} (syntax was added in Python {})",
|
"{kind} on Python {} (syntax was {changed})",
|
||||||
self.target_version,
|
self.target_version,
|
||||||
self.kind.minimum_version(),
|
changed = self.kind.changed_version(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnsupportedSyntaxErrorKind {
|
/// Represents the kind of change in Python syntax between versions.
|
||||||
/// The earliest allowed version for the syntax associated with this error.
|
enum Change {
|
||||||
pub const fn minimum_version(&self) -> PythonVersion {
|
Added(PythonVersion),
|
||||||
|
Removed(PythonVersion),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Change {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
|
Change::Added(version) => write!(f, "added in Python {version}"),
|
||||||
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
|
Change::Removed(version) => write!(f, "removed in Python {version}"),
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UnsupportedSyntaxErrorKind {
|
||||||
|
/// 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 => 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")]
|
#[cfg(target_pointer_width = "64")]
|
||||||
mod sizes {
|
mod sizes {
|
||||||
use crate::error::{LexicalError, LexicalErrorType};
|
use crate::error::{LexicalError, LexicalErrorType};
|
||||||
|
|
|
@ -702,9 +702,31 @@ impl<'src> Parser<'src> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let arg_range = parser.node_range(start);
|
||||||
if parser.eat(TokenKind::Equal) {
|
if parser.eat(TokenKind::Equal) {
|
||||||
seen_keyword_argument = true;
|
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)
|
||||||
|
// f((a) = 1)
|
||||||
|
// f( ( a ) = 1)
|
||||||
|
|
||||||
|
if is_parenthesized {
|
||||||
|
parser.add_unsupported_syntax_error(
|
||||||
|
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName,
|
||||||
|
arg_range,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ast::Identifier {
|
ast::Identifier {
|
||||||
id: ident_expr.id,
|
id: ident_expr.id,
|
||||||
range: ident_expr.range,
|
range: ident_expr.range,
|
||||||
|
|
|
@ -441,7 +441,7 @@ impl<'src> Parser<'src> {
|
||||||
/// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and
|
/// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and
|
||||||
/// [`TextRange`] if its minimum version is less than [`Parser::target_version`].
|
/// [`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) {
|
||||||
if self.options.target_version < kind.minimum_version() {
|
if kind.is_unsupported(self.options.target_version) {
|
||||||
self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
|
self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
|
||||||
kind,
|
kind,
|
||||||
range,
|
range,
|
||||||
|
|
|
@ -424,7 +424,7 @@ impl<'src> Parser<'src> {
|
||||||
/// are only allowed in Python 3.8 and later: <https://github.com/python/cpython/issues/76298>.
|
/// 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) {
|
pub(crate) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: StarTupleKind) {
|
||||||
let kind = UnsupportedSyntaxErrorKind::StarTuple(kind);
|
let kind = UnsupportedSyntaxErrorKind::StarTuple(kind);
|
||||||
if self.options.target_version >= kind.minimum_version() {
|
if kind.is_supported(self.options.target_version) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_kwarg_py37.py
|
||||||
|
---
|
||||||
|
## AST
|
||||||
|
|
||||||
|
```
|
||||||
|
Module(
|
||||||
|
ModModule {
|
||||||
|
range: 0..52,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
Loading…
Add table
Add a link
Reference in a new issue