Skip to content

Commit 7fb5f47

Browse files
Respect # noqa directives on __all__ openers (#10798)
## Summary Historically, given: ```python __all__ = [ # noqa: F822 "Bernoulli", "Beta", "Binomial", ] ``` The F822 violations would be attached to the `__all__`, so this `# noqa` would be enforced for _all_ definitions in the list. This changed in #10525 for the better, in that we now use the range of each string. But these `# noqa` directives stopped working. This PR sets the `__all__` as a parent range in the diagnostic, so that these directives are respected once again. Closes #10795. ## Test Plan `cargo test`
1 parent 83db62b commit 7fb5f47

File tree

6 files changed

+102
-33
lines changed

6 files changed

+102
-33
lines changed

crates/ruff_diagnostics/src/diagnostic.rs

+8
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ impl Diagnostic {
7171
}
7272
}
7373

74+
/// Consumes `self` and returns a new `Diagnostic` with the given parent node.
75+
#[inline]
76+
#[must_use]
77+
pub fn with_parent(mut self, parent: TextSize) -> Self {
78+
self.set_parent(parent);
79+
self
80+
}
81+
7482
/// Set the location of the diagnostic's parent node.
7583
#[inline]
7684
pub fn set_parent(&mut self, parent: TextSize) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Respect `# noqa` directives on `__all__` definitions."""
2+
3+
__all__ = [ # noqa: F822
4+
"Bernoulli",
5+
"Beta",
6+
"Binomial",
7+
]
8+
9+
10+
__all__ += [
11+
"ContinuousBernoulli", # noqa: F822
12+
"ExponentialFamily",
13+
]

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

+41-33
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,14 @@ use std::path::Path;
3131
use itertools::Itertools;
3232
use log::debug;
3333
use ruff_python_ast::{
34-
self as ast, all::DunderAllName, Comprehension, ElifElseClause, ExceptHandler, Expr,
35-
ExprContext, FStringElement, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters,
36-
Pattern, Stmt, Suite, UnaryOp,
34+
self as ast, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, FStringElement,
35+
Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
3736
};
3837
use ruff_text_size::{Ranged, TextRange, TextSize};
3938

4039
use ruff_diagnostics::{Diagnostic, IsolationLevel};
4140
use ruff_notebook::{CellOffsets, NotebookIndex};
42-
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
41+
use ruff_python_ast::all::{extract_all_names, DunderAllDefinition, DunderAllFlags};
4342
use ruff_python_ast::helpers::{
4443
collect_import_from_member, extract_handled_exceptions, is_docstring_stmt, to_module_path,
4544
};
@@ -2109,45 +2108,54 @@ impl<'a> Checker<'a> {
21092108
fn visit_exports(&mut self) {
21102109
let snapshot = self.semantic.snapshot();
21112110

2112-
let exports: Vec<DunderAllName> = self
2111+
let definitions: Vec<DunderAllDefinition> = self
21132112
.semantic
21142113
.global_scope()
21152114
.get_all("__all__")
21162115
.map(|binding_id| &self.semantic.bindings[binding_id])
21172116
.filter_map(|binding| match &binding.kind {
2118-
BindingKind::Export(Export { names }) => Some(names.iter().copied()),
2117+
BindingKind::Export(Export { names }) => {
2118+
Some(DunderAllDefinition::new(binding.range(), names.to_vec()))
2119+
}
21192120
_ => None,
21202121
})
2121-
.flatten()
21222122
.collect();
21232123

2124-
for export in exports {
2125-
let (name, range) = (export.name(), export.range());
2126-
if let Some(binding_id) = self.semantic.global_scope().get(name) {
2127-
self.semantic.flags |= SemanticModelFlags::DUNDER_ALL_DEFINITION;
2128-
// Mark anything referenced in `__all__` as used.
2129-
self.semantic
2130-
.add_global_reference(binding_id, ExprContext::Load, range);
2131-
self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION;
2132-
} else {
2133-
if self.semantic.global_scope().uses_star_imports() {
2134-
if self.enabled(Rule::UndefinedLocalWithImportStarUsage) {
2135-
self.diagnostics.push(Diagnostic::new(
2136-
pyflakes::rules::UndefinedLocalWithImportStarUsage {
2137-
name: name.to_string(),
2138-
},
2139-
range,
2140-
));
2141-
}
2124+
for definition in definitions {
2125+
for export in definition.names() {
2126+
let (name, range) = (export.name(), export.range());
2127+
if let Some(binding_id) = self.semantic.global_scope().get(name) {
2128+
self.semantic.flags |= SemanticModelFlags::DUNDER_ALL_DEFINITION;
2129+
// Mark anything referenced in `__all__` as used.
2130+
self.semantic
2131+
.add_global_reference(binding_id, ExprContext::Load, range);
2132+
self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION;
21422133
} else {
2143-
if self.enabled(Rule::UndefinedExport) {
2144-
if !self.path.ends_with("__init__.py") {
2145-
self.diagnostics.push(Diagnostic::new(
2146-
pyflakes::rules::UndefinedExport {
2147-
name: name.to_string(),
2148-
},
2149-
range,
2150-
));
2134+
if self.semantic.global_scope().uses_star_imports() {
2135+
if self.enabled(Rule::UndefinedLocalWithImportStarUsage) {
2136+
self.diagnostics.push(
2137+
Diagnostic::new(
2138+
pyflakes::rules::UndefinedLocalWithImportStarUsage {
2139+
name: name.to_string(),
2140+
},
2141+
range,
2142+
)
2143+
.with_parent(definition.start()),
2144+
);
2145+
}
2146+
} else {
2147+
if self.enabled(Rule::UndefinedExport) {
2148+
if !self.path.ends_with("__init__.py") {
2149+
self.diagnostics.push(
2150+
Diagnostic::new(
2151+
pyflakes::rules::UndefinedExport {
2152+
name: name.to_string(),
2153+
},
2154+
range,
2155+
)
2156+
.with_parent(definition.start()),
2157+
);
2158+
}
21512159
}
21522160
}
21532161
}

crates/ruff_linter/src/rules/pyflakes/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ mod tests {
162162
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
163163
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
164164
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]
165+
#[test_case(Rule::UndefinedExport, Path::new("F822_3.py"))]
165166
#[test_case(Rule::UndefinedLocal, Path::new("F823.py"))]
166167
#[test_case(Rule::UnusedVariable, Path::new("F841_0.py"))]
167168
#[test_case(Rule::UnusedVariable, Path::new("F841_1.py"))]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
3+
---
4+
F822_3.py:12:5: F822 Undefined name `ExponentialFamily` in `__all__`
5+
|
6+
10 | __all__ += [
7+
11 | "ContinuousBernoulli", # noqa: F822
8+
12 | "ExponentialFamily",
9+
| ^^^^^^^^^^^^^^^^^^^ F822
10+
13 | ]
11+
|

crates/ruff_python_ast/src/all.rs

+28
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@ impl Ranged for DunderAllName<'_> {
3939
}
4040
}
4141

42+
/// Abstraction for a collection of names inside an `__all__` definition,
43+
/// e.g. `["foo", "bar"]` in `__all__ = ["foo", "bar"]`
44+
#[derive(Debug, Clone)]
45+
pub struct DunderAllDefinition<'a> {
46+
/// The range of the `__all__` identifier.
47+
range: TextRange,
48+
/// The names inside the `__all__` definition.
49+
names: Vec<DunderAllName<'a>>,
50+
}
51+
52+
impl<'a> DunderAllDefinition<'a> {
53+
/// Initialize a new [`DunderAllDefinition`] instance.
54+
pub fn new(range: TextRange, names: Vec<DunderAllName<'a>>) -> Self {
55+
Self { range, names }
56+
}
57+
58+
/// The names inside the `__all__` definition.
59+
pub fn names(&self) -> &[DunderAllName<'a>] {
60+
&self.names
61+
}
62+
}
63+
64+
impl Ranged for DunderAllDefinition<'_> {
65+
fn range(&self) -> TextRange {
66+
self.range
67+
}
68+
}
69+
4270
/// Extract the names bound to a given __all__ assignment.
4371
///
4472
/// Accepts a closure that determines whether a given name (e.g., `"list"`) is a Python builtin.

0 commit comments

Comments
 (0)