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..581beed7f3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py @@ -0,0 +1,30 @@ +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 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..98735e9b5d --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap @@ -0,0 +1,56 @@ +--- +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 + | diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 0c7ceef4a4..29bc07c47c 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_function_scope() || target.start() < global_start { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::AnnotatedGlobal(id.to_string()), + target.range(), + ); + } + } + } } Stmt::FunctionDef(ast::StmtFunctionDef { type_params,