From 52849a5e689dd83d3752ebfa63840a0cc3224bcc Mon Sep 17 00:00:00 2001 From: Bhuminjay Soni Date: Wed, 17 Dec 2025 19:09:47 +0530 Subject: [PATCH] [syntax-errors] Annotated name cannot be global (#20868) ## Summary This PR implements a new semantic syntax error where annotated name can't be global example ``` x: int = 1 def f(): global x x: str = "foo" # SyntaxError: annotated name 'x' can't be global ``` ## Test Plan I have written tests as directed in #17412 --------- Signed-off-by: 11happy Signed-off-by: 11happy Co-authored-by: Brent Westbrook --- .../semantic_errors/annotated_global.py | 38 ++++++++++ crates/ruff_linter/src/linter.rs | 1 + ...syntax_error_annotated_global.py_3.14.snap | 74 +++++++++++++++++++ .../ruff_python_parser/src/semantic_errors.rs | 16 +++- 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py new file mode 100644 index 0000000000..8e1c29d8fa --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py @@ -0,0 +1,38 @@ +a: int = 1 +def f1(): + global a + a: str = "foo" # error + +b: int = 1 +def outer(): + def inner(): + global b + b: str = "nested" # error + +c: int = 1 +def f2(): + global c + c: list[str] = [] # error + +d: int = 1 +def f3(): + global d + d: str # error + +e: int = 1 +def f4(): + e: str = "happy" # okay + +global f +f: int = 1 # okay + +g: int = 1 +global g # error + +class C: + x: str + global x # error + +class D: + global x # error + x: str \ No newline at end of file diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 08c0417020..f3880076af 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1001,6 +1001,7 @@ mod tests { #[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)] #[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)] #[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)] + #[test_case(Path::new("annotated_global.py"), PythonVersion::PY314)] fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> { let snapshot = format!( "semantic_syntax_error_{}_{}", diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap new file mode 100644 index 0000000000..bc8140b385 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap @@ -0,0 +1,74 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: annotated name `a` can't be global + --> resources/test/fixtures/semantic_errors/annotated_global.py:4:5 + | +2 | def f1(): +3 | global a +4 | a: str = "foo" # error + | ^ +5 | +6 | b: int = 1 + | + +invalid-syntax: annotated name `b` can't be global + --> resources/test/fixtures/semantic_errors/annotated_global.py:10:9 + | + 8 | def inner(): + 9 | global b +10 | b: str = "nested" # error + | ^ +11 | +12 | c: int = 1 + | + +invalid-syntax: annotated name `c` can't be global + --> resources/test/fixtures/semantic_errors/annotated_global.py:15:5 + | +13 | def f2(): +14 | global c +15 | c: list[str] = [] # error + | ^ +16 | +17 | d: int = 1 + | + +invalid-syntax: annotated name `d` can't be global + --> resources/test/fixtures/semantic_errors/annotated_global.py:20:5 + | +18 | def f3(): +19 | global d +20 | d: str # error + | ^ +21 | +22 | e: int = 1 + | + +invalid-syntax: annotated name `g` can't be global + --> resources/test/fixtures/semantic_errors/annotated_global.py:29:1 + | +27 | f: int = 1 # okay +28 | +29 | g: int = 1 + | ^ +30 | global g # error + | + +invalid-syntax: annotated name `x` can't be global + --> resources/test/fixtures/semantic_errors/annotated_global.py:33:5 + | +32 | class C: +33 | x: str + | ^ +34 | global x # error + | + +invalid-syntax: annotated name `x` can't be global + --> resources/test/fixtures/semantic_errors/annotated_global.py:38:5 + | +36 | class D: +37 | global x # error +38 | x: str + | ^ + | diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 0c7ceef4a4..c58ac07a32 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -272,7 +272,9 @@ impl SemanticSyntaxChecker { fn check_annotation(stmt: &ast::Stmt, ctx: &Ctx) { match stmt { - Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) => { + Stmt::AnnAssign(ast::StmtAnnAssign { + target, annotation, .. + }) => { if ctx.python_version() > PythonVersion::PY313 { // test_ok valid_annotation_py313 // # parse_options: {"target-version": "3.13"} @@ -297,6 +299,18 @@ impl SemanticSyntaxChecker { }; visitor.visit_expr(annotation); } + if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + if let Some(global_stmt) = ctx.global(id.as_str()) { + let global_start = global_stmt.start(); + if !ctx.in_module_scope() || target.start() < global_start { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::AnnotatedGlobal(id.to_string()), + target.range(), + ); + } + } + } } Stmt::FunctionDef(ast::StmtFunctionDef { type_params,