From 16dcacd979326a7493fcfbbb33889085d364c75b Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas Date: Thu, 24 Apr 2025 09:16:44 +0530 Subject: [PATCH] [syntax-errors] nonlocal declaration not allowed module level --- crates/ruff_linter/src/checkers/ast/mod.rs | 3 +- .../nonlocal_declaration_at_module_level.py | 2 + .../inline/err/nonlocal_stmt_empty.py | 3 +- .../inline/err/nonlocal_stmt_expression.py | 3 +- .../err/nonlocal_stmt_trailing_comma.py | 7 +- .../nonlocal_declaration_at_module_level.py | 2 + .../resources/inline/ok/nonlocal_stmt.py | 5 +- .../src/parser/statement.rs | 18 +-- .../ruff_python_parser/src/semantic_errors.rs | 22 ++++ crates/ruff_python_parser/tests/fixtures.rs | 2 +- ...nlocal_declaration_at_module_level.py.snap | 55 ++++++++ ...invalid_syntax@nonlocal_stmt_empty.py.snap | 39 ++++-- ...id_syntax@nonlocal_stmt_expression.py.snap | 85 ++++++++----- ...yntax@nonlocal_stmt_trailing_comma.py.snap | 119 +++++++++++------- ...nlocal_declaration_at_module_level.py.snap | 49 ++++++++ .../valid_syntax@nonlocal_stmt.py.snap | 80 +++++++----- 16 files changed, 363 insertions(+), 131 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 87169d7a91028..82f3ea339e1c9 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -616,7 +616,8 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::InvalidStarExpression | SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) - | SemanticSyntaxErrorKind::DuplicateParameter(_) => { + | SemanticSyntaxErrorKind::DuplicateParameter(_) + | SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { if self.settings.preview.is_enabled() { self.semantic_errors.borrow_mut().push(error); } diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py new file mode 100644 index 0000000000000..b7ed1ba1b0b9e --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py @@ -0,0 +1,2 @@ +nonlocal x +nonlocal x, y diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py index 07127b5f052d5..f6f2b5577aca3 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py @@ -1 +1,2 @@ -nonlocal +def _(): + nonlocal diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py index 303cb88b61d23..46854180075b6 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py @@ -1 +1,2 @@ -nonlocal x + 1 +def _(): + nonlocal x + 1 diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py index 24acf55245280..d0742c873124d 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py @@ -1,3 +1,4 @@ -nonlocal , -nonlocal x, -nonlocal x, y, +def _(): + nonlocal , + nonlocal x, + nonlocal x, y, diff --git a/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py b/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py new file mode 100644 index 0000000000000..8f51257d0e03b --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py @@ -0,0 +1,2 @@ +def _(): + nonlocal x diff --git a/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py b/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py index 7f652bb0a69f1..81caa434cb26f 100644 --- a/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py +++ b/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py @@ -1,2 +1,3 @@ -nonlocal x -nonlocal x, y, z +def _(): + nonlocal x + nonlocal x, y, z diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 2361b252e6b8a..cb1d4d5e570ba 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -897,12 +897,14 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Nonlocal); // test_err nonlocal_stmt_trailing_comma - // nonlocal , - // nonlocal x, - // nonlocal x, y, + // def _(): + // nonlocal , + // nonlocal x, + // nonlocal x, y, // test_err nonlocal_stmt_expression - // nonlocal x + 1 + // def _(): + // nonlocal x + 1 let names = self.parse_comma_separated_list_into_vec( RecoveryContextKind::Identifiers, Parser::parse_identifier, @@ -910,7 +912,8 @@ impl<'src> Parser<'src> { if names.is_empty() { // test_err nonlocal_stmt_empty - // nonlocal + // def _(): + // nonlocal self.add_error( ParseErrorType::EmptyNonlocalNames, self.current_token_range(), @@ -918,8 +921,9 @@ impl<'src> Parser<'src> { } // test_ok nonlocal_stmt - // nonlocal x - // nonlocal x, y, z + // def _(): + // nonlocal x + // nonlocal x, y, z ast::StmtNonlocal { range: self.node_range(start), names, diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index fd41ea21879f9..6858163ebd767 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -142,6 +142,22 @@ impl SemanticSyntaxChecker { AwaitOutsideAsyncFunctionKind::AsyncWith, ); } + Stmt::Nonlocal(ast::StmtNonlocal { range, .. }) => { + // test_ok nonlocal_declaration_at_module_level + // def _(): + // nonlocal x + + // test_err nonlocal_declaration_at_module_level + // nonlocal x + // nonlocal x, y + if ctx.in_module_scope() { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel, + *range, + ); + } + } _ => {} } @@ -933,6 +949,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::DuplicateParameter(name) => { write!(f, r#"Duplicate parameter "{name}""#) } + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { + write!(f, "nonlocal declaration not allowed at module level") + } } } } @@ -1254,6 +1273,9 @@ pub enum SemanticSyntaxErrorKind { /// lambda x, x: ... /// ``` DuplicateParameter(String), + + /// Represents a nonlocal declaration at module level + NonlocalDeclarationAtModuleLevel, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 1ba0f39f3f33b..f81cbcd41e094 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -538,7 +538,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { } fn in_module_scope(&self) -> bool { - true + self.scopes.len() == 1 } fn in_function_scope(&self) -> bool { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap new file mode 100644 index 0000000000000..8b63405c85239 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py +--- +## AST + +``` +Module( + ModModule { + range: 0..25, + body: [ + Nonlocal( + StmtNonlocal { + range: 0..10, + names: [ + Identifier { + id: Name("x"), + range: 9..10, + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + range: 11..24, + names: [ + Identifier { + id: Name("x"), + range: 20..21, + }, + Identifier { + id: Name("y"), + range: 23..24, + }, + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | nonlocal x + | ^^^^^^^^^^ Syntax Error: nonlocal declaration not allowed at module level +2 | nonlocal x, y + | + + + | +1 | nonlocal x +2 | nonlocal x, y + | ^^^^^^^^^^^^^ Syntax Error: nonlocal declaration not allowed at module level + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap index 08015c08ab20e..67c9ca6335fa2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap @@ -1,19 +1,41 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..9, + range: 0..22, body: [ - Nonlocal( - StmtNonlocal { - range: 0..8, - names: [], + FunctionDef( + StmtFunctionDef { + range: 0..21, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..21, + names: [], + }, + ), + ], }, ), ], @@ -23,6 +45,7 @@ Module( ## Errors | -1 | nonlocal - | ^ Syntax Error: Nonlocal statement must have at least one name +1 | def _(): +2 | nonlocal + | ^ Syntax Error: Nonlocal statement must have at least one name | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap index 7903a99470370..b5d768d9a32e3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap @@ -1,45 +1,67 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..15, + range: 0..28, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [ - Identifier { - id: Name("x"), - range: 9..10, - }, + FunctionDef( + StmtFunctionDef { + range: 0..27, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + }, + ], + }, + ), + Expr( + StmtExpr { + range: 24..27, + value: UnaryOp( + ExprUnaryOp { + range: 24..27, + op: UAdd, + operand: NumberLiteral( + ExprNumberLiteral { + range: 26..27, + value: Int( + 1, + ), + }, + ), + }, + ), + }, + ), ], }, ), - Expr( - StmtExpr { - range: 11..14, - value: UnaryOp( - ExprUnaryOp { - range: 11..14, - op: UAdd, - operand: NumberLiteral( - ExprNumberLiteral { - range: 13..14, - value: Int( - 1, - ), - }, - ), - }, - ), - }, - ), ], }, ) @@ -47,6 +69,7 @@ Module( ## Errors | -1 | nonlocal x + 1 - | ^ Syntax Error: Simple statements must be separated by newlines or semicolons +1 | def _(): +2 | nonlocal x + 1 + | ^ Syntax Error: Simple statements must be separated by newlines or semicolons | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap index 4a901178b0a95..ee76a9c939bb7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap @@ -1,44 +1,66 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..38, + range: 0..59, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [], - }, - ), - Nonlocal( - StmtNonlocal { - range: 11..22, - names: [ - Identifier { - id: Name("x"), - range: 20..21, - }, - ], - }, - ), - Nonlocal( - StmtNonlocal { - range: 23..37, - names: [ - Identifier { - id: Name("x"), - range: 32..33, - }, - Identifier { - id: Name("y"), - range: 35..36, - }, + FunctionDef( + StmtFunctionDef { + range: 0..58, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [], + }, + ), + Nonlocal( + StmtNonlocal { + range: 28..39, + names: [ + Identifier { + id: Name("x"), + range: 37..38, + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + range: 44..58, + names: [ + Identifier { + id: Name("x"), + range: 53..54, + }, + Identifier { + id: Name("y"), + range: 56..57, + }, + ], + }, + ), ], }, ), @@ -49,32 +71,35 @@ Module( ## Errors | -1 | nonlocal , - | ^ Syntax Error: Expected an identifier -2 | nonlocal x, -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , + | ^ Syntax Error: Expected an identifier +3 | nonlocal x, +4 | nonlocal x, y, | | -1 | nonlocal , - | ^ Syntax Error: Nonlocal statement must have at least one name -2 | nonlocal x, -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , + | ^ Syntax Error: Nonlocal statement must have at least one name +3 | nonlocal x, +4 | nonlocal x, y, | | -1 | nonlocal , -2 | nonlocal x, - | ^ Syntax Error: Trailing comma not allowed -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , +3 | nonlocal x, + | ^ Syntax Error: Trailing comma not allowed +4 | nonlocal x, y, | | -1 | nonlocal , -2 | nonlocal x, -3 | nonlocal x, y, - | ^ Syntax Error: Trailing comma not allowed +2 | nonlocal , +3 | nonlocal x, +4 | nonlocal x, y, + | ^ Syntax Error: Trailing comma not allowed | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap new file mode 100644 index 0000000000000..b1646e4b6cc09 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py +--- +## AST + +``` +Module( + ModModule { + range: 0..24, + body: [ + FunctionDef( + StmtFunctionDef { + range: 0..23, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + }, + ], + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap index 33287486f8ae0..f7b7d0a1c8cbf 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap @@ -1,42 +1,64 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..28, + range: 0..45, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [ - Identifier { - id: Name("x"), - range: 9..10, - }, - ], - }, - ), - Nonlocal( - StmtNonlocal { - range: 11..27, - names: [ - Identifier { - id: Name("x"), - range: 20..21, - }, - Identifier { - id: Name("y"), - range: 23..24, - }, - Identifier { - id: Name("z"), - range: 26..27, - }, + FunctionDef( + StmtFunctionDef { + range: 0..44, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + range: 28..44, + names: [ + Identifier { + id: Name("x"), + range: 37..38, + }, + Identifier { + id: Name("y"), + range: 40..41, + }, + Identifier { + id: Name("z"), + range: 43..44, + }, + ], + }, + ), ], }, ),