Improve ruff_python_semantic::all::extract_all_names() (#11335)

This commit is contained in:
Alex Waygood 2024-05-08 17:09:31 +01:00 committed by GitHub
parent 4541337f3d
commit dfe4291c0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 111 additions and 92 deletions

View file

@ -0,0 +1,4 @@
import builtins
a = 1
__all__ = builtins.list(["a", "b"])

View file

@ -49,7 +49,7 @@ use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Stylist}; use ruff_python_codegen::{Generator, Stylist};
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind}; use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::all::{extract_all_names, DunderAllDefinition, DunderAllFlags}; use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags};
use ruff_python_semantic::analyze::{imports, typing}; use ruff_python_semantic::analyze::{imports, typing};
use ruff_python_semantic::{ use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module, BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
@ -1882,8 +1882,7 @@ impl<'a> Checker<'a> {
_ => false, _ => false,
} }
{ {
let (all_names, all_flags) = let (all_names, all_flags) = self.semantic.extract_dunder_all_names(parent);
extract_all_names(parent, |name| self.semantic.has_builtin_binding(name));
if all_flags.intersects(DunderAllFlags::INVALID_OBJECT) { if all_flags.intersects(DunderAllFlags::INVALID_OBJECT) {
flags |= BindingFlags::INVALID_ALL_OBJECT; flags |= BindingFlags::INVALID_ALL_OBJECT;

View file

@ -161,6 +161,7 @@ mod tests {
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1b.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_3.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_3.py"))]
#[test_case(Rule::UndefinedLocal, Path::new("F823.py"))] #[test_case(Rule::UndefinedLocal, Path::new("F823.py"))]

View file

@ -0,0 +1,10 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F822_1b.py:4:31: F822 Undefined name `b` in `__all__`
|
2 | a = 1
3 |
4 | __all__ = builtins.list(["a", "b"])
| ^^^ F822
|

View file

@ -5,7 +5,6 @@ use std::fmt::Debug;
use std::ops::Deref; use std::ops::Deref;
use std::path::Path; use std::path::Path;
use crate::all::DunderAllName;
use ruff_index::{newtype_index, IndexSlice, IndexVec}; use ruff_index::{newtype_index, IndexSlice, IndexVec};
use ruff_python_ast::{self as ast, Stmt}; use ruff_python_ast::{self as ast, Stmt};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -13,6 +12,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::analyze::visibility::{ use crate::analyze::visibility::{
class_visibility, function_visibility, method_visibility, module_visibility, Visibility, class_visibility, function_visibility, method_visibility, module_visibility, Visibility,
}; };
use crate::model::all::DunderAllName;
/// Id uniquely identifying a definition in a program. /// Id uniquely identifying a definition in a program.
#[newtype_index] #[newtype_index]

View file

@ -1,4 +1,3 @@
pub mod all;
pub mod analyze; pub mod analyze;
mod binding; mod binding;
mod branches; mod branches;

View file

@ -1,3 +1,5 @@
pub mod all;
use std::path::Path; use std::path::Path;
use bitflags::bitflags; use bitflags::bitflags;

View file

@ -1,8 +1,12 @@
//! Utilities for semantic analysis of `__all__` definitions
use bitflags::bitflags; use bitflags::bitflags;
use ruff_python_ast::{self as ast, helpers::map_subscript, Expr, Stmt}; use ruff_python_ast::{self as ast, helpers::map_subscript, Expr, Stmt};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::SemanticModel;
bitflags! { bitflags! {
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
pub struct DunderAllFlags: u8 { pub struct DunderAllFlags: u8 {
@ -66,34 +70,79 @@ impl Ranged for DunderAllDefinition<'_> {
} }
} }
/// Extract the names bound to a given __all__ assignment. impl<'a> SemanticModel<'a> {
/// /// 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_dunder_all_names<'expr>(
pub fn extract_all_names<F>(stmt: &Stmt, is_builtin: F) -> (Vec<DunderAllName>, DunderAllFlags) &self,
where stmt: &'expr Stmt,
F: Fn(&str) -> bool, ) -> (Vec<DunderAllName<'expr>>, DunderAllFlags) {
{ fn add_to_names<'expr>(
fn add_to_names<'a>( elts: &'expr [Expr],
elts: &'a [Expr], names: &mut Vec<DunderAllName<'expr>>,
names: &mut Vec<DunderAllName<'a>>, flags: &mut DunderAllFlags,
flags: &mut DunderAllFlags, ) {
) { for elt in elts {
for elt in elts { if let Expr::StringLiteral(ast::ExprStringLiteral { value, range }) = elt {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, range }) = elt { names.push(DunderAllName {
names.push(DunderAllName { name: value.to_str(),
name: value.to_str(), range: *range,
range: *range, });
}); } else {
} else { *flags |= DunderAllFlags::INVALID_OBJECT;
*flags |= DunderAllFlags::INVALID_OBJECT; }
} }
} }
let mut names: Vec<DunderAllName> = vec![];
let mut flags = DunderAllFlags::empty();
if let Some(value) = match stmt {
Stmt::Assign(ast::StmtAssign { value, .. }) => Some(value),
Stmt::AnnAssign(ast::StmtAnnAssign { value, .. }) => value.as_ref(),
Stmt::AugAssign(ast::StmtAugAssign { value, .. }) => Some(value),
_ => None,
} {
if let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = value.as_ref() {
let mut current_left = left;
let mut current_right = right;
loop {
// Process the right side, which should be a "real" value.
let (elts, new_flags) = self.extract_dunder_all_elts(current_right);
flags |= new_flags;
if let Some(elts) = elts {
add_to_names(elts, &mut names, &mut flags);
}
// Process the left side, which can be a "real" value or the "rest" of the
// binary operation.
if let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = current_left.as_ref() {
current_left = left;
current_right = right;
} else {
let (elts, new_flags) = self.extract_dunder_all_elts(current_left);
flags |= new_flags;
if let Some(elts) = elts {
add_to_names(elts, &mut names, &mut flags);
}
break;
}
}
} else {
let (elts, new_flags) = self.extract_dunder_all_elts(value);
flags |= new_flags;
if let Some(elts) = elts {
add_to_names(elts, &mut names, &mut flags);
}
}
}
(names, flags)
} }
fn extract_elts<F>(expr: &Expr, is_builtin: F) -> (Option<&[Expr]>, DunderAllFlags) fn extract_dunder_all_elts<'expr>(
where &self,
F: Fn(&str) -> bool, expr: &'expr Expr,
{ ) -> (Option<&'expr [Expr]>, DunderAllFlags) {
match expr { match expr {
Expr::List(ast::ExprList { elts, .. }) => { Expr::List(ast::ExprList { elts, .. }) => {
return (Some(elts), DunderAllFlags::empty()); return (Some(elts), DunderAllFlags::empty());
@ -122,24 +171,24 @@ where
}) => { }) => {
// Allow `tuple()`, `list()`, and their generic forms, like `list[int]()`. // Allow `tuple()`, `list()`, and their generic forms, like `list[int]()`.
if arguments.keywords.is_empty() && arguments.args.len() <= 1 { if arguments.keywords.is_empty() && arguments.args.len() <= 1 {
if let Expr::Name(ast::ExprName { id, .. }) = map_subscript(func) { if self
let id = id.as_str(); .resolve_builtin_symbol(map_subscript(func))
if matches!(id, "tuple" | "list") && is_builtin(id) { .is_some_and(|symbol| matches!(symbol, "tuple" | "list"))
let [arg] = arguments.args.as_ref() else { {
let [arg] = arguments.args.as_ref() else {
return (None, DunderAllFlags::empty());
};
match arg {
Expr::List(ast::ExprList { elts, .. })
| Expr::Set(ast::ExprSet { elts, .. })
| Expr::Tuple(ast::ExprTuple { elts, .. }) => {
return (Some(elts), DunderAllFlags::empty());
}
_ => {
// We can't analyze other expressions, but they must be
// valid, since the `list` or `tuple` call will ultimately
// evaluate to a list or tuple.
return (None, DunderAllFlags::empty()); return (None, DunderAllFlags::empty());
};
match arg {
Expr::List(ast::ExprList { elts, .. })
| Expr::Set(ast::ExprSet { elts, .. })
| Expr::Tuple(ast::ExprTuple { elts, .. }) => {
return (Some(elts), DunderAllFlags::empty());
}
_ => {
// We can't analyze other expressions, but they must be
// valid, since the `list` or `tuple` call will ultimately
// evaluate to a list or tuple.
return (None, DunderAllFlags::empty());
}
} }
} }
} }
@ -147,55 +196,10 @@ where
} }
Expr::Named(ast::ExprNamed { value, .. }) => { Expr::Named(ast::ExprNamed { value, .. }) => {
// Allow, e.g., `__all__ += (value := ["A", "B"])`. // Allow, e.g., `__all__ += (value := ["A", "B"])`.
return extract_elts(value, is_builtin); return self.extract_dunder_all_elts(value);
} }
_ => {} _ => {}
} }
(None, DunderAllFlags::INVALID_FORMAT) (None, DunderAllFlags::INVALID_FORMAT)
} }
let mut names: Vec<DunderAllName> = vec![];
let mut flags = DunderAllFlags::empty();
if let Some(value) = match stmt {
Stmt::Assign(ast::StmtAssign { value, .. }) => Some(value),
Stmt::AnnAssign(ast::StmtAnnAssign { value, .. }) => value.as_ref(),
Stmt::AugAssign(ast::StmtAugAssign { value, .. }) => Some(value),
_ => None,
} {
if let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = value.as_ref() {
let mut current_left = left;
let mut current_right = right;
loop {
// Process the right side, which should be a "real" value.
let (elts, new_flags) = extract_elts(current_right, |expr| is_builtin(expr));
flags |= new_flags;
if let Some(elts) = elts {
add_to_names(elts, &mut names, &mut flags);
}
// Process the left side, which can be a "real" value or the "rest" of the
// binary operation.
if let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = current_left.as_ref() {
current_left = left;
current_right = right;
} else {
let (elts, new_flags) = extract_elts(current_left, |expr| is_builtin(expr));
flags |= new_flags;
if let Some(elts) = elts {
add_to_names(elts, &mut names, &mut flags);
}
break;
}
}
} else {
let (elts, new_flags) = extract_elts(value, |expr| is_builtin(expr));
flags |= new_flags;
if let Some(elts) = elts {
add_to_names(elts, &mut names, &mut flags);
}
}
}
(names, flags)
} }