mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-23 16:51:58 +00:00
Fix syntax error false positives on parenthesized context managers (#20846)
This PR resolves the issue noticed in https://github.com/astral-sh/ruff/pull/20777#discussion_r2417233227. Namely, cases like this were being flagged as syntax errors despite being perfectly valid on Python 3.8: ```pycon Python 3.8.20 (default, Oct 2 2024, 16:34:12) [Clang 18.1.8 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>> with (open("foo.txt", "w")): ... ... Ellipsis >>> with (open("foo.txt", "w")) as f: print(f) ... <_io.TextIOWrapper name='foo.txt' mode='w' encoding='UTF-8'> ``` The second of these was already allowed but not the first: ```shell > ruff check --target-version py38 --ignore ALL - <<EOF with (open("foo.txt", "w")): ... with (open("foo.txt", "w")) as f: print(f) EOF invalid-syntax: Cannot use parentheses within a `with` statement on Python 3.8 (syntax was added in Python 3.9) --> -:1:6 | 1 | with (open("foo.txt", "w")): ... | ^ 2 | with (open("foo.txt", "w")) as f: print(f) | Found 1 error. ``` There was some discussion of related cases in https://github.com/astral-sh/ruff/pull/16523#discussion_r1984657793, but it seems I overlooked the single-element case when flagging tuples. As suggested in the other thread, we can just check if there's more than one element or a trailing comma, which will cause the tuple parsing on <=3.8 and avoid the false positives.
This commit is contained in:
parent
373fe8a39c
commit
71f8389f61
6 changed files with 222 additions and 149 deletions
|
@ -2130,7 +2130,7 @@ impl<'src> Parser<'src> {
|
|||
let open_paren_range = self.current_token_range();
|
||||
|
||||
if self.at(TokenKind::Lpar) {
|
||||
if let Some(items) = self.try_parse_parenthesized_with_items() {
|
||||
if let (Some(items), has_trailing_comma) = self.try_parse_parenthesized_with_items() {
|
||||
// test_ok tuple_context_manager_py38
|
||||
// # parse_options: {"target-version": "3.8"}
|
||||
// with (
|
||||
|
@ -2139,6 +2139,13 @@ impl<'src> Parser<'src> {
|
|||
// baz,
|
||||
// ) as tup: ...
|
||||
|
||||
// test_ok single_parenthesized_item_context_manager_py38
|
||||
// # parse_options: {"target-version": "3.8"}
|
||||
// with (
|
||||
// open('foo.txt')) as foo: ...
|
||||
// with (
|
||||
// open('foo.txt')): ...
|
||||
|
||||
// test_err tuple_context_manager_py38
|
||||
// # parse_options: {"target-version": "3.8"}
|
||||
// # these cases are _syntactically_ valid before Python 3.9 because the `with` item
|
||||
|
@ -2146,8 +2153,6 @@ impl<'src> Parser<'src> {
|
|||
// # anyway
|
||||
// with (foo, bar): ...
|
||||
// with (
|
||||
// open('foo.txt')) as foo: ...
|
||||
// with (
|
||||
// foo,
|
||||
// bar,
|
||||
// baz,
|
||||
|
@ -2165,10 +2170,12 @@ impl<'src> Parser<'src> {
|
|||
// with (foo as x, bar as y): ...
|
||||
// with (foo, bar as y): ...
|
||||
// with (foo as x, bar): ...
|
||||
self.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::ParenthesizedContextManager,
|
||||
open_paren_range,
|
||||
);
|
||||
if items.len() > 1 || has_trailing_comma {
|
||||
self.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::ParenthesizedContextManager,
|
||||
open_paren_range,
|
||||
);
|
||||
}
|
||||
|
||||
self.expect(TokenKind::Rpar);
|
||||
items
|
||||
|
@ -2228,7 +2235,7 @@ impl<'src> Parser<'src> {
|
|||
/// If the parser isn't positioned at a `(` token.
|
||||
///
|
||||
/// See: <https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-with_stmt_contents>
|
||||
fn try_parse_parenthesized_with_items(&mut self) -> Option<Vec<WithItem>> {
|
||||
fn try_parse_parenthesized_with_items(&mut self) -> (Option<Vec<WithItem>>, bool) {
|
||||
let checkpoint = self.checkpoint();
|
||||
|
||||
// We'll start with the assumption that the with items are parenthesized.
|
||||
|
@ -2245,11 +2252,12 @@ impl<'src> Parser<'src> {
|
|||
// with (item1, item2 item3, item4): ...
|
||||
// with (item1, item2 as f1 item3, item4): ...
|
||||
// with (item1, item2: ...
|
||||
self.parse_comma_separated_list(RecoveryContextKind::WithItems(with_item_kind), |p| {
|
||||
let parsed_with_item = p.parse_with_item(WithItemParsingState::Speculative);
|
||||
has_optional_vars |= parsed_with_item.item.optional_vars.is_some();
|
||||
parsed_with_items.push(parsed_with_item);
|
||||
});
|
||||
let has_trailing_comma =
|
||||
self.parse_comma_separated_list(RecoveryContextKind::WithItems(with_item_kind), |p| {
|
||||
let parsed_with_item = p.parse_with_item(WithItemParsingState::Speculative);
|
||||
has_optional_vars |= parsed_with_item.item.optional_vars.is_some();
|
||||
parsed_with_items.push(parsed_with_item);
|
||||
});
|
||||
|
||||
// Check if our assumption is incorrect and it's actually a parenthesized expression.
|
||||
if has_optional_vars {
|
||||
|
@ -2319,7 +2327,7 @@ impl<'src> Parser<'src> {
|
|||
with_item_kind = WithItemKind::ParenthesizedExpression;
|
||||
}
|
||||
|
||||
if with_item_kind.is_parenthesized() {
|
||||
let with_items = if with_item_kind.is_parenthesized() {
|
||||
Some(
|
||||
parsed_with_items
|
||||
.into_iter()
|
||||
|
@ -2330,7 +2338,9 @@ impl<'src> Parser<'src> {
|
|||
self.rewind(checkpoint);
|
||||
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
(with_items, has_trailing_comma)
|
||||
}
|
||||
|
||||
/// Parses a single `with` item.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue