[syntax-errors] nonlocal declaration at module level (#17559)

## Summary

Part of #17412

Add a new compile-time syntax error for detecting `nonlocal`
declarations at a module level.

## Test Plan

- Added new inline tests for the syntax error
- Updated existing tests for `nonlocal` statement parsing to be inside a
function scope

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
Abhijeet Prasad Bodas 2025-04-25 01:41:46 +05:30 committed by GitHub
parent 538393d1f3
commit cf59cee928
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 363 additions and 131 deletions

View file

@ -616,7 +616,8 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_)
| SemanticSyntaxErrorKind::InvalidStarExpression | SemanticSyntaxErrorKind::InvalidStarExpression
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_) | SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
| SemanticSyntaxErrorKind::DuplicateParameter(_) => { | SemanticSyntaxErrorKind::DuplicateParameter(_)
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
if self.settings.preview.is_enabled() { if self.settings.preview.is_enabled() {
self.semantic_errors.borrow_mut().push(error); self.semantic_errors.borrow_mut().push(error);
} }

View file

@ -0,0 +1,2 @@
nonlocal x
nonlocal x, y

View file

@ -1 +1,2 @@
nonlocal def _():
nonlocal

View file

@ -1 +1,2 @@
nonlocal x + 1 def _():
nonlocal x + 1

View file

@ -1,3 +1,4 @@
nonlocal , def _():
nonlocal x, nonlocal ,
nonlocal x, y, nonlocal x,
nonlocal x, y,

View file

@ -0,0 +1,2 @@
def _():
nonlocal x

View file

@ -1,2 +1,3 @@
nonlocal x def _():
nonlocal x, y, z nonlocal x
nonlocal x, y, z

View file

@ -897,12 +897,14 @@ impl<'src> Parser<'src> {
self.bump(TokenKind::Nonlocal); self.bump(TokenKind::Nonlocal);
// test_err nonlocal_stmt_trailing_comma // test_err nonlocal_stmt_trailing_comma
// nonlocal , // def _():
// nonlocal x, // nonlocal ,
// nonlocal x, y, // nonlocal x,
// nonlocal x, y,
// test_err nonlocal_stmt_expression // test_err nonlocal_stmt_expression
// nonlocal x + 1 // def _():
// nonlocal x + 1
let names = self.parse_comma_separated_list_into_vec( let names = self.parse_comma_separated_list_into_vec(
RecoveryContextKind::Identifiers, RecoveryContextKind::Identifiers,
Parser::parse_identifier, Parser::parse_identifier,
@ -910,7 +912,8 @@ impl<'src> Parser<'src> {
if names.is_empty() { if names.is_empty() {
// test_err nonlocal_stmt_empty // test_err nonlocal_stmt_empty
// nonlocal // def _():
// nonlocal
self.add_error( self.add_error(
ParseErrorType::EmptyNonlocalNames, ParseErrorType::EmptyNonlocalNames,
self.current_token_range(), self.current_token_range(),
@ -918,8 +921,9 @@ impl<'src> Parser<'src> {
} }
// test_ok nonlocal_stmt // test_ok nonlocal_stmt
// nonlocal x // def _():
// nonlocal x, y, z // nonlocal x
// nonlocal x, y, z
ast::StmtNonlocal { ast::StmtNonlocal {
range: self.node_range(start), range: self.node_range(start),
names, names,

View file

@ -142,6 +142,22 @@ impl SemanticSyntaxChecker {
AwaitOutsideAsyncFunctionKind::AsyncWith, 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) => { SemanticSyntaxErrorKind::DuplicateParameter(name) => {
write!(f, r#"Duplicate parameter "{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: ... /// lambda x, x: ...
/// ``` /// ```
DuplicateParameter(String), DuplicateParameter(String),
/// Represents a nonlocal declaration at module level
NonlocalDeclarationAtModuleLevel,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

View file

@ -538,7 +538,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
} }
fn in_module_scope(&self) -> bool { fn in_module_scope(&self) -> bool {
true self.scopes.len() == 1
} }
fn in_function_scope(&self) -> bool { fn in_function_scope(&self) -> bool {

View file

@ -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
|

View file

@ -1,19 +1,41 @@
--- ---
source: crates/ruff_python_parser/tests/fixtures.rs source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py
snapshot_kind: text
--- ---
## AST ## AST
``` ```
Module( Module(
ModModule { ModModule {
range: 0..9, range: 0..22,
body: [ body: [
Nonlocal( FunctionDef(
StmtNonlocal { StmtFunctionDef {
range: 0..8, range: 0..21,
names: [], 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 ## Errors
| |
1 | nonlocal 1 | def _():
| ^ Syntax Error: Nonlocal statement must have at least one name 2 | nonlocal
| ^ Syntax Error: Nonlocal statement must have at least one name
| |

View file

@ -1,45 +1,67 @@
--- ---
source: crates/ruff_python_parser/tests/fixtures.rs source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py
snapshot_kind: text
--- ---
## AST ## AST
``` ```
Module( Module(
ModModule { ModModule {
range: 0..15, range: 0..28,
body: [ body: [
Nonlocal( FunctionDef(
StmtNonlocal { StmtFunctionDef {
range: 0..10, range: 0..27,
names: [ is_async: false,
Identifier { decorator_list: [],
id: Name("x"), name: Identifier {
range: 9..10, 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 ## Errors
| |
1 | nonlocal x + 1 1 | def _():
| ^ Syntax Error: Simple statements must be separated by newlines or semicolons 2 | nonlocal x + 1
| ^ Syntax Error: Simple statements must be separated by newlines or semicolons
| |

View file

@ -1,44 +1,66 @@
--- ---
source: crates/ruff_python_parser/tests/fixtures.rs source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py
snapshot_kind: text
--- ---
## AST ## AST
``` ```
Module( Module(
ModModule { ModModule {
range: 0..38, range: 0..59,
body: [ body: [
Nonlocal( FunctionDef(
StmtNonlocal { StmtFunctionDef {
range: 0..10, range: 0..58,
names: [], is_async: false,
}, decorator_list: [],
), name: Identifier {
Nonlocal( id: Name("_"),
StmtNonlocal { range: 4..5,
range: 11..22, },
names: [ type_params: None,
Identifier { parameters: Parameters {
id: Name("x"), range: 5..7,
range: 20..21, posonlyargs: [],
}, args: [],
], vararg: None,
}, kwonlyargs: [],
), kwarg: None,
Nonlocal( },
StmtNonlocal { returns: None,
range: 23..37, body: [
names: [ Nonlocal(
Identifier { StmtNonlocal {
id: Name("x"), range: 13..23,
range: 32..33, names: [],
}, },
Identifier { ),
id: Name("y"), Nonlocal(
range: 35..36, 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 ## Errors
| |
1 | nonlocal , 1 | def _():
| ^ Syntax Error: Expected an identifier 2 | nonlocal ,
2 | nonlocal x, | ^ Syntax Error: Expected an identifier
3 | nonlocal x, y, 3 | nonlocal x,
4 | nonlocal x, y,
| |
| |
1 | nonlocal , 1 | def _():
| ^ Syntax Error: Nonlocal statement must have at least one name 2 | nonlocal ,
2 | nonlocal x, | ^ Syntax Error: Nonlocal statement must have at least one name
3 | nonlocal x, y, 3 | nonlocal x,
4 | nonlocal x, y,
| |
| |
1 | nonlocal , 1 | def _():
2 | nonlocal x, 2 | nonlocal ,
| ^ Syntax Error: Trailing comma not allowed 3 | nonlocal x,
3 | nonlocal x, y, | ^ Syntax Error: Trailing comma not allowed
4 | nonlocal x, y,
| |
| |
1 | nonlocal , 2 | nonlocal ,
2 | nonlocal x, 3 | nonlocal x,
3 | nonlocal x, y, 4 | nonlocal x, y,
| ^ Syntax Error: Trailing comma not allowed | ^ Syntax Error: Trailing comma not allowed
| |

View file

@ -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,
},
],
},
),
],
},
),
],
},
)
```

View file

@ -1,42 +1,64 @@
--- ---
source: crates/ruff_python_parser/tests/fixtures.rs source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py
snapshot_kind: text
--- ---
## AST ## AST
``` ```
Module( Module(
ModModule { ModModule {
range: 0..28, range: 0..45,
body: [ body: [
Nonlocal( FunctionDef(
StmtNonlocal { StmtFunctionDef {
range: 0..10, range: 0..44,
names: [ is_async: false,
Identifier { decorator_list: [],
id: Name("x"), name: Identifier {
range: 9..10, id: Name("_"),
}, range: 4..5,
], },
}, type_params: None,
), parameters: Parameters {
Nonlocal( range: 5..7,
StmtNonlocal { posonlyargs: [],
range: 11..27, args: [],
names: [ vararg: None,
Identifier { kwonlyargs: [],
id: Name("x"), kwarg: None,
range: 20..21, },
}, returns: None,
Identifier { body: [
id: Name("y"), Nonlocal(
range: 23..24, StmtNonlocal {
}, range: 13..23,
Identifier { names: [
id: Name("z"), Identifier {
range: 26..27, 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,
},
],
},
),
], ],
}, },
), ),