Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track ranges of names inside __all__ definitions #10525

Merged
merged 4 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/ruff_linter/src/checkers/ast/analyze/definitions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ruff_python_ast::str::raw_contents_range;
use ruff_python_ast::{all::DunderAllName, str::raw_contents_range};
use ruff_text_size::{Ranged, TextRange};

use ruff_python_semantic::{
Expand Down Expand Up @@ -93,7 +93,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
}

// Compute visibility of all definitions.
let exports: Option<Vec<&str>> = {
let exports: Option<Vec<DunderAllName>> = {
checker
.semantic
.global_scope()
Expand Down
15 changes: 6 additions & 9 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ use std::path::Path;
use itertools::Itertools;
use log::debug;
use ruff_python_ast::{
self as ast, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, Keyword,
MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
self as ast, all::DunderAllName, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt,
Suite, UnaryOp,
};
use ruff_text_size::{Ranged, TextRange, TextSize};

Expand Down Expand Up @@ -2097,25 +2098,21 @@ impl<'a> Checker<'a> {
fn visit_exports(&mut self) {
let snapshot = self.semantic.snapshot();

let exports: Vec<(&str, TextRange)> = self
let exports: Vec<DunderAllName> = self
.semantic
.global_scope()
.get_all("__all__")
.map(|binding_id| &self.semantic.bindings[binding_id])
.filter_map(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => {
Some(names.iter().map(|name| (*name, binding.range())))
}
BindingKind::Export(Export { names }) => Some(names.iter().copied()),
_ => None,
})
.flatten()
.collect();

for (name, range) in exports {
for DunderAllName { name, range } in exports {
if let Some(binding_id) = self.semantic.global_scope().get(name) {
// Mark anything referenced in `__all__` as used.
// TODO(charlie): `range` here should be the range of the name in `__all__`, not
// the range of `__all__` itself.
self.semantic
.add_global_reference(binding_id, ExprContext::Load, range);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ F405.py:5:11: F405 `name` may be undefined, or defined from star imports
| ^^^^ F405
|

F405.py:11:1: F405 `a` may be undefined, or defined from star imports
F405.py:11:12: F405 `a` may be undefined, or defined from star imports
|
9 | print(name)
10 |
11 | __all__ = ['a']
| ^^^^^^^ F405
| ^^^ F405
|


Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F822_0.py:3:1: F822 Undefined name `b` in `__all__`
F822_0.py:3:17: F822 Undefined name `b` in `__all__`
|
1 | a = 1
2 |
3 | __all__ = ["a", "b"]
| ^^^^^^^ F822
| ^^^ F822
|


Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F822_0.pyi:4:1: F822 Undefined name `c` in `__all__`
F822_0.pyi:4:22: F822 Undefined name `c` in `__all__`
|
2 | b: int # Considered a binding in a `.pyi` stub file, not in a `.py` runtime file
3 |
4 | __all__ = ["a", "b", "c"] # c is flagged as missing; b is not
| ^^^^^^^ F822
| ^^^ F822
|
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F822_1.py:3:1: F822 Undefined name `b` in `__all__`
F822_1.py:3:22: F822 Undefined name `b` in `__all__`
|
1 | a = 1
2 |
3 | __all__ = list(["a", "b"])
| ^^^^^^^ F822
| ^^^ F822
|


35 changes: 30 additions & 5 deletions crates/ruff_python_ast/src/all.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bitflags::bitflags;
use ruff_text_size::{Ranged, TextRange};

use crate::helpers::map_subscript;
use crate::{self as ast, Expr, Stmt};
Expand All @@ -15,17 +16,41 @@ bitflags! {
}
}

/// Abstraction for a string inside an `__all__` definition,
/// e.g. `"foo"` in `__all__ = ["foo"]`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DunderAllName<'a> {
/// The value of the string inside the `__all__` definition
pub name: &'a str,
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved

/// The range of the string inside the `__all__` definition
pub range: TextRange,
}

impl Ranged for DunderAllName<'_> {
fn range(&self) -> TextRange {
self.range
}
}

/// Extract the names bound to a given __all__ assignment.
///
/// Accepts a closure that determines whether a given name (e.g., `"list"`) is a Python builtin.
pub fn extract_all_names<F>(stmt: &Stmt, is_builtin: F) -> (Vec<&str>, DunderAllFlags)
pub fn extract_all_names<F>(stmt: &Stmt, is_builtin: F) -> (Vec<DunderAllName>, DunderAllFlags)
where
F: Fn(&str) -> bool,
{
fn add_to_names<'a>(elts: &'a [Expr], names: &mut Vec<&'a str>, flags: &mut DunderAllFlags) {
fn add_to_names<'a>(
elts: &'a [Expr],
names: &mut Vec<DunderAllName<'a>>,
flags: &mut DunderAllFlags,
) {
for elt in elts {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
names.push(value.to_str());
if let Expr::StringLiteral(ast::ExprStringLiteral { value, range }) = elt {
names.push(DunderAllName {
name: value.to_str(),
range: *range,
});
} else {
*flags |= DunderAllFlags::INVALID_OBJECT;
}
Expand Down Expand Up @@ -96,7 +121,7 @@ where
(None, DunderAllFlags::INVALID_FORMAT)
}

let mut names: Vec<&str> = vec![];
let mut names: Vec<DunderAllName> = vec![];
let mut flags = DunderAllFlags::empty();

if let Some(value) = match stmt {
Expand Down
3 changes: 2 additions & 1 deletion crates/ruff_python_semantic/src/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::ops::{Deref, DerefMut};
use bitflags::bitflags;

use ruff_index::{newtype_index, IndexSlice, IndexVec};
use ruff_python_ast::all::DunderAllName;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::Stmt;
use ruff_source_file::Locator;
Expand Down Expand Up @@ -370,7 +371,7 @@ impl<'a> FromIterator<Binding<'a>> for Bindings<'a> {
#[derive(Debug, Clone)]
pub struct Export<'a> {
/// The names of the bindings exported via `__all__`.
pub names: Box<[&'a str]>,
pub names: Box<[DunderAllName<'a>]>,
}

/// A binding for an `import`, keyed on the name to which the import is bound.
Expand Down
12 changes: 8 additions & 4 deletions crates/ruff_python_semantic/src/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::fmt::Debug;
use std::ops::Deref;

use ruff_index::{newtype_index, IndexSlice, IndexVec};
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_ast::{self as ast, all::DunderAllName, Stmt};
use ruff_text_size::{Ranged, TextRange};

use crate::analyze::visibility::{
Expand Down Expand Up @@ -187,7 +187,7 @@ impl<'a> Definitions<'a> {
}

/// Resolve the visibility of each definition in the collection.
pub fn resolve(self, exports: Option<&[&str]>) -> ContextualizedDefinitions<'a> {
pub fn resolve(self, exports: Option<&[DunderAllName]>) -> ContextualizedDefinitions<'a> {
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
let mut definitions: IndexVec<DefinitionId, ContextualizedDefinition<'a>> =
IndexVec::with_capacity(self.len());

Expand All @@ -201,7 +201,9 @@ impl<'a> Definitions<'a> {
MemberKind::Class(class) => {
let parent = &definitions[member.parent];
if parent.visibility.is_private()
|| exports.is_some_and(|exports| !exports.contains(&member.name()))
|| exports.is_some_and(|exports| {
!exports.iter().any(|export| export.name == member.name())
})
{
Visibility::Private
} else {
Expand All @@ -221,7 +223,9 @@ impl<'a> Definitions<'a> {
MemberKind::Function(function) => {
let parent = &definitions[member.parent];
if parent.visibility.is_private()
|| exports.is_some_and(|exports| !exports.contains(&member.name()))
|| exports.is_some_and(|exports| {
!exports.iter().any(|export| export.name == member.name())
})
{
Visibility::Private
} else {
Expand Down