Skip to content

Commit 61a7466

Browse files
ntBreMichaReiser
authored andcommitted
[syntax-errors] Reimplement PLE0118 (astral-sh#17135)
Summary -- This PR reimplements [load-before-global-declaration (PLE0118)](https://docs.astral.sh/ruff/rules/load-before-global-declaration/) as a semantic syntax error. I added a `global` method to the `SemanticSyntaxContext` trait to make this very easy, at least in ruff. Does red-knot have something similar? If this approach will also work in red-knot, I think some of the other PLE rules are also compile-time errors in CPython, PLE0117 in particular. 0115 and 0116 also mention `SyntaxError`s in their docs, but I haven't confirmed them in the REPL yet. Test Plan -- Existing linter tests for PLE0118. I think this actually can't be tested very easily in an inline test because the `TestContext` doesn't have a real way to track globals. --------- Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent d8a185e commit 61a7466

File tree

5 files changed

+62
-27
lines changed

5 files changed

+62
-27
lines changed

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
337337
if checker.enabled(Rule::UndocumentedWarn) {
338338
flake8_logging::rules::undocumented_warn(checker, expr);
339339
}
340-
if checker.enabled(Rule::LoadBeforeGlobalDeclaration) {
341-
pylint::rules::load_before_global_declaration(checker, id, expr);
342-
}
343340
}
344341
Expr::Attribute(attribute) => {
345342
if attribute.ctx == ExprContext::Load {

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ use crate::noqa::NoqaMapping;
6767
use crate::package::PackageRoot;
6868
use crate::registry::Rule;
6969
use crate::rules::pyflakes::rules::LateFutureImport;
70+
use crate::rules::pylint::rules::LoadBeforeGlobalDeclaration;
7071
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
7172
use crate::settings::{flags, LinterSettings};
7273
use crate::{docstrings, noqa, Locator};
@@ -540,13 +541,32 @@ impl SemanticSyntaxContext for Checker<'_> {
540541
self.target_version
541542
}
542543

544+
fn global(&self, name: &str) -> Option<TextRange> {
545+
self.semantic.global(name)
546+
}
547+
543548
fn report_semantic_error(&self, error: SemanticSyntaxError) {
544549
match error.kind {
545550
SemanticSyntaxErrorKind::LateFutureImport => {
546551
if self.settings.rules.enabled(Rule::LateFutureImport) {
547552
self.report_diagnostic(Diagnostic::new(LateFutureImport, error.range));
548553
}
549554
}
555+
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start } => {
556+
if self
557+
.settings
558+
.rules
559+
.enabled(Rule::LoadBeforeGlobalDeclaration)
560+
{
561+
self.report_diagnostic(Diagnostic::new(
562+
LoadBeforeGlobalDeclaration {
563+
name,
564+
row: self.compute_source_row(start),
565+
},
566+
error.range,
567+
));
568+
}
569+
}
550570
SemanticSyntaxErrorKind::ReboundComprehensionVariable
551571
| SemanticSyntaxErrorKind::DuplicateTypeParameter
552572
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)

crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
use ruff_python_ast::Expr;
2-
3-
use ruff_diagnostics::{Diagnostic, Violation};
1+
use ruff_diagnostics::Violation;
42
use ruff_macros::{derive_message_formats, ViolationMetadata};
53
use ruff_source_file::SourceRow;
6-
use ruff_text_size::Ranged;
7-
8-
use crate::checkers::ast::Checker;
94

105
/// ## What it does
116
/// Checks for uses of names that are declared as `global` prior to the
@@ -42,8 +37,8 @@ use crate::checkers::ast::Checker;
4237
/// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement)
4338
#[derive(ViolationMetadata)]
4439
pub(crate) struct LoadBeforeGlobalDeclaration {
45-
name: String,
46-
row: SourceRow,
40+
pub(crate) name: String,
41+
pub(crate) row: SourceRow,
4742
}
4843

4944
impl Violation for LoadBeforeGlobalDeclaration {
@@ -53,18 +48,3 @@ impl Violation for LoadBeforeGlobalDeclaration {
5348
format!("Name `{name}` is used prior to global declaration on {row}")
5449
}
5550
}
56-
57-
/// PLE0118
58-
pub(crate) fn load_before_global_declaration(checker: &Checker, name: &str, expr: &Expr) {
59-
if let Some(stmt) = checker.semantic().global(name) {
60-
if expr.start() < stmt.start() {
61-
checker.report_diagnostic(Diagnostic::new(
62-
LoadBeforeGlobalDeclaration {
63-
name: name.to_string(),
64-
row: checker.compute_source_row(stmt.start()),
65-
},
66-
expr.range(),
67-
));
68-
}
69-
}
70-
}

crates/ruff_python_parser/src/semantic_errors.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use ruff_python_ast::{
1212
Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr,
1313
StmtImportFrom,
1414
};
15-
use ruff_text_size::{Ranged, TextRange};
15+
use ruff_text_size::{Ranged, TextRange, TextSize};
1616
use rustc_hash::FxHashSet;
1717

1818
#[derive(Debug)]
@@ -397,6 +397,21 @@ impl SemanticSyntaxChecker {
397397
_ => {}
398398
};
399399
}
400+
401+
// PLE0118
402+
if let Some(stmt) = ctx.global(id) {
403+
let start = stmt.start();
404+
if expr.start() < start {
405+
Self::add_error(
406+
ctx,
407+
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration {
408+
name: id.to_string(),
409+
start,
410+
},
411+
expr.range(),
412+
);
413+
}
414+
}
400415
}
401416
Expr::Yield(ast::ExprYield {
402417
value: Some(value), ..
@@ -499,6 +514,9 @@ impl Display for SemanticSyntaxError {
499514
write!(f, "cannot delete `__debug__` on Python {python_version} (syntax was removed in 3.9)")
500515
}
501516
},
517+
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => {
518+
write!(f, "name `{name}` is used prior to global declaration")
519+
}
502520
SemanticSyntaxErrorKind::InvalidStarExpression => {
503521
f.write_str("can't use starred expression here")
504522
}
@@ -616,6 +634,19 @@ pub enum SemanticSyntaxErrorKind {
616634
/// [BPO 45000]: https://github.com/python/cpython/issues/89163
617635
WriteToDebug(WriteToDebugKind),
618636

637+
/// Represents the use of a `global` variable before its `global` declaration.
638+
///
639+
/// ## Examples
640+
///
641+
/// ```python
642+
/// counter = 1
643+
/// def increment():
644+
/// print(f"Adding 1 to {counter}")
645+
/// global counter
646+
/// counter += 1
647+
/// ```
648+
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
649+
619650
/// Represents the use of a starred expression in an invalid location, such as a `return` or
620651
/// `yield` statement.
621652
///
@@ -758,6 +789,9 @@ pub trait SemanticSyntaxContext {
758789
/// The target Python version for detecting backwards-incompatible syntax changes.
759790
fn python_version(&self) -> PythonVersion;
760791

792+
/// Return the [`TextRange`] at which a name is declared as `global` in the current scope.
793+
fn global(&self, name: &str) -> Option<TextRange>;
794+
761795
fn report_semantic_error(&self, error: SemanticSyntaxError);
762796
}
763797

crates/ruff_python_parser/tests/fixtures.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,4 +488,8 @@ impl SemanticSyntaxContext for TestContext {
488488
fn report_semantic_error(&self, error: SemanticSyntaxError) {
489489
self.diagnostics.borrow_mut().push(error);
490490
}
491+
492+
fn global(&self, _name: &str) -> Option<TextRange> {
493+
None
494+
}
491495
}

0 commit comments

Comments
 (0)