Skip to content

Commit 1fbc4e1

Browse files
WhiteFox0-0George-Ogden
authored andcommitted
[syntax-errors] Name is parameter and nonlocal (astral-sh#27628)
## Summary Part of astral-sh#17412 Detects semantic syntax error where name is parameter and nonlocal ## Test Plan Added tests in `nonlocal_parameter.py`
1 parent 67d3638 commit 1fbc4e1

7 files changed

Lines changed: 170 additions & 2 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
def f(a):
2+
nonlocal a
3+
4+
def g(a):
5+
if True:
6+
nonlocal a
7+
8+
def h(a):
9+
def inner():
10+
nonlocal a
11+
12+
def i(a):
13+
try:
14+
nonlocal a
15+
except Exception:
16+
pass
17+
18+
def f(a):
19+
a = 1
20+
a = 2
21+
nonlocal a
22+
23+
def f(a):
24+
class Inner:
25+
nonlocal a # ok
26+
27+
def f(a):
28+
def inner(a):
29+
nonlocal a
30+
31+
def f(a=1):
32+
def inner():
33+
nonlocal a # ok

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ impl SemanticSyntaxContext for Checker<'_> {
810810
| SemanticSyntaxErrorKind::DifferentMatchPatternBindings
811811
| SemanticSyntaxErrorKind::InvalidExpression(..)
812812
| SemanticSyntaxErrorKind::GlobalParameter(_)
813+
| SemanticSyntaxErrorKind::NonlocalParameter(_)
813814
| SemanticSyntaxErrorKind::DuplicateMatchKey(_)
814815
| SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_)
815816
| SemanticSyntaxErrorKind::InvalidStarExpression

crates/ruff_linter/src/linter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,7 @@ mod tests {
10321032
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)]
10331033
#[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)]
10341034
#[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)]
1035+
#[test_case(Path::new("nonlocal_parameter.py"), PythonVersion::PY310)]
10351036
#[test_case(Path::new("annotated_global.py"), PythonVersion::PY314)]
10361037
#[test_case(Path::new("lazy_future_import.py"), PythonVersion::PY315)]
10371038
fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
source: crates/ruff_linter/src/linter.rs
3+
---
4+
invalid-syntax: name `a` cannot refer to a parameter and a nonlocal variable
5+
--> resources/test/fixtures/semantic_errors/nonlocal_parameter.py:2:14
6+
|
7+
1 | def f(a):
8+
2 | nonlocal a
9+
| ^
10+
3 |
11+
4 | def g(a):
12+
|
13+
14+
invalid-syntax: name `a` cannot refer to a parameter and a nonlocal variable
15+
--> resources/test/fixtures/semantic_errors/nonlocal_parameter.py:6:18
16+
|
17+
4 | def g(a):
18+
5 | if True:
19+
6 | nonlocal a
20+
| ^
21+
7 |
22+
8 | def h(a):
23+
|
24+
25+
invalid-syntax: name `a` cannot refer to a parameter and a nonlocal variable
26+
--> resources/test/fixtures/semantic_errors/nonlocal_parameter.py:14:18
27+
|
28+
12 | def i(a):
29+
13 | try:
30+
14 | nonlocal a
31+
| ^
32+
15 | except Exception:
33+
16 | pass
34+
|
35+
36+
invalid-syntax: name `a` cannot refer to a parameter and a nonlocal variable
37+
--> resources/test/fixtures/semantic_errors/nonlocal_parameter.py:21:14
38+
|
39+
19 | a = 1
40+
20 | a = 2
41+
21 | nonlocal a
42+
| ^
43+
22 |
44+
23 | def f(a):
45+
|
46+
47+
invalid-syntax: name `a` cannot refer to a parameter and a nonlocal variable
48+
--> resources/test/fixtures/semantic_errors/nonlocal_parameter.py:29:18
49+
|
50+
27 | def f(a):
51+
28 | def inner(a):
52+
29 | nonlocal a
53+
| ^
54+
30 |
55+
31 | def f(a=1):
56+
|

crates/ruff_python_parser/src/semantic_errors.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,13 @@ impl SemanticSyntaxChecker {
324324

325325
if !ctx.in_module_scope() {
326326
for name in names {
327-
if !ctx.has_nonlocal_binding(name) {
327+
if ctx.is_bound_parameter(name) {
328+
Self::add_error(
329+
ctx,
330+
SemanticSyntaxErrorKind::NonlocalParameter(name.to_string()),
331+
name.range,
332+
);
333+
} else if !ctx.has_nonlocal_binding(name) {
328334
Self::add_error(
329335
ctx,
330336
SemanticSyntaxErrorKind::NonlocalWithoutBinding(name.to_string()),
@@ -1432,6 +1438,12 @@ impl Display for SemanticSyntaxError {
14321438
"name `{name}` cannot refer to a parameter and a global variable"
14331439
)
14341440
}
1441+
SemanticSyntaxErrorKind::NonlocalParameter(name) => {
1442+
write!(
1443+
f,
1444+
"name `{name}` cannot refer to a parameter and a nonlocal variable"
1445+
)
1446+
}
14351447
SemanticSyntaxErrorKind::DifferentMatchPatternBindings => {
14361448
write!(f, "alternative patterns bind different names")
14371449
}
@@ -1871,6 +1883,13 @@ pub enum SemanticSyntaxErrorKind {
18711883
/// ambiguity and will result in a `SyntaxError`.
18721884
GlobalParameter(String),
18731885

1886+
/// Represents a function parameter that is also declared as `nonlocal`.
1887+
///
1888+
/// Declaring a parameter as `nonlocal` is invalid, since parameters are already
1889+
/// bound in a local scope of the function. using `nonlocal` on them introduces
1890+
/// ambiguity and will result in a `SyntaxError`.
1891+
NonlocalParameter(String),
1892+
18741893
/// Represents the use of alternative patterns in a `match` statement that bind different names.
18751894
///
18761895
/// Python requires all alternatives in an OR pattern (`|`) to bind the same set of names.

crates/ty_python_core/src/builder.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4589,7 +4589,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
45894589
let symbol_id = self.add_symbol(name.id.clone());
45904590
let symbol = self.current_place_table().symbol(symbol_id);
45914591
// Check whether the variable has already been accessed in this scope.
4592-
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
4592+
if (symbol.is_bound() || symbol.is_declared() || symbol.is_used())
4593+
&& !symbol.is_parameter()
4594+
{
45934595
self.report_semantic_error(SemanticSyntaxError {
45944596
kind: SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration {
45954597
name: name.to_string(),

crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,3 +603,59 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl
603603
27 | global a # snapshot: invalid-syntax
604604
| ^
605605
```
606+
607+
## name cannot refer to a parameter and a nonlocal variable
608+
609+
```py
610+
a = None
611+
612+
def outer():
613+
a = None
614+
def f(a):
615+
nonlocal a # snapshot: invalid-syntax
616+
617+
def outer():
618+
a = None
619+
def g(a):
620+
if True:
621+
nonlocal a # error: [invalid-syntax]
622+
623+
def h(a):
624+
def inner():
625+
nonlocal a
626+
627+
def outer():
628+
a = None
629+
def i(a):
630+
try:
631+
nonlocal a # error: [invalid-syntax]
632+
except Exception:
633+
pass
634+
635+
def outer():
636+
a = None
637+
def f(a):
638+
a = 1
639+
a = 2
640+
nonlocal a # error: [invalid-syntax]
641+
642+
def f(a):
643+
class Inner:
644+
nonlocal a
645+
646+
def f(a):
647+
def inner(a):
648+
nonlocal a # error: [invalid-syntax]
649+
650+
def f(a=1):
651+
def inner():
652+
nonlocal a
653+
```
654+
655+
```snapshot
656+
error[invalid-syntax]: name `a` cannot refer to a parameter and a nonlocal variable
657+
--> src/mdtest_snippet.py:6:18
658+
|
659+
6 | nonlocal a # snapshot: invalid-syntax
660+
| ^
661+
```

0 commit comments

Comments
 (0)