mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +00:00
Respect mixed variable assignment in RET504 (#4835)
This commit is contained in:
parent
c14896b42c
commit
42c071d302
3 changed files with 98 additions and 33 deletions
|
@ -272,3 +272,34 @@ def str_to_bool(val):
|
||||||
if isinstance(val, bool):
|
if isinstance(val, bool):
|
||||||
return some_obj
|
return some_obj
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
# Mixed assignments
|
||||||
|
def function_assignment(x):
|
||||||
|
def f(): ...
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def class_assignment(x):
|
||||||
|
class Foo: ...
|
||||||
|
|
||||||
|
return Foo
|
||||||
|
|
||||||
|
|
||||||
|
def mixed_function_assignment(x):
|
||||||
|
if x:
|
||||||
|
def f(): ...
|
||||||
|
else:
|
||||||
|
f = 42
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def mixed_class_assignment(x):
|
||||||
|
if x:
|
||||||
|
class Foo: ...
|
||||||
|
else:
|
||||||
|
Foo = 42
|
||||||
|
|
||||||
|
return Foo
|
||||||
|
|
|
@ -506,34 +506,36 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the `id` has multiple assignments within the function.
|
/// Return `true` if the `id` has multiple declarations within the function.
|
||||||
fn has_multiple_assigns(id: &str, stack: &Stack) -> bool {
|
fn has_multiple_declarations(id: &str, stack: &Stack) -> bool {
|
||||||
if let Some(assigns) = stack.assignments.get(&id) {
|
stack
|
||||||
if assigns.len() > 1 {
|
.declarations
|
||||||
return true;
|
.get(&id)
|
||||||
}
|
.map_or(false, |declarations| declarations.len() > 1)
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the `id` has a (read) reference between the `return_location` and its
|
/// Return `true` if the `id` has a (read) reference between the `return_location` and its
|
||||||
/// preceding assignment.
|
/// preceding declaration.
|
||||||
fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) -> bool {
|
fn has_references_before_next_declaration(
|
||||||
let mut assignment_before_return: Option<TextSize> = None;
|
id: &str,
|
||||||
let mut assignment_after_return: Option<TextSize> = None;
|
return_range: TextRange,
|
||||||
if let Some(assignments) = stack.assignments.get(&id) {
|
stack: &Stack,
|
||||||
|
) -> bool {
|
||||||
|
let mut declaration_before_return: Option<TextSize> = None;
|
||||||
|
let mut declaration_after_return: Option<TextSize> = None;
|
||||||
|
if let Some(assignments) = stack.declarations.get(&id) {
|
||||||
for location in assignments.iter().sorted() {
|
for location in assignments.iter().sorted() {
|
||||||
if *location > return_range.start() {
|
if *location > return_range.start() {
|
||||||
assignment_after_return = Some(*location);
|
declaration_after_return = Some(*location);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
assignment_before_return = Some(*location);
|
declaration_before_return = Some(*location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no assignment before the return, then the variable must be defined in
|
// If there is no declaration before the return, then the variable must be declared in
|
||||||
// some other way (e.g., a function argument). No need to check for references.
|
// some other way (e.g., a function argument). No need to check for references.
|
||||||
let Some(assignment_before_return) = assignment_before_return else {
|
let Some(declaration_before_return) = declaration_before_return else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -543,9 +545,9 @@ fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if assignment_before_return < *location {
|
if declaration_before_return < *location {
|
||||||
if let Some(assignment_after_return) = assignment_after_return {
|
if let Some(declaration_after_return) = declaration_after_return {
|
||||||
if *location <= assignment_after_return {
|
if *location <= declaration_after_return {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -559,7 +561,7 @@ fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the `id` has a read or write reference within a `try` or loop body.
|
/// Return `true` if the `id` has a read or write reference within a `try` or loop body.
|
||||||
fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool {
|
fn has_references_or_declarations_within_try_or_loop(id: &str, stack: &Stack) -> bool {
|
||||||
if let Some(references) = stack.references.get(&id) {
|
if let Some(references) = stack.references.get(&id) {
|
||||||
for location in references {
|
for location in references {
|
||||||
for try_range in &stack.tries {
|
for try_range in &stack.tries {
|
||||||
|
@ -574,7 +576,7 @@ fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(references) = stack.assignments.get(&id) {
|
if let Some(references) = stack.declarations.get(&id) {
|
||||||
for location in references {
|
for location in references {
|
||||||
for try_range in &stack.tries {
|
for try_range in &stack.tries {
|
||||||
if try_range.contains(*location) {
|
if try_range.contains(*location) {
|
||||||
|
@ -594,7 +596,7 @@ fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool {
|
||||||
/// RET504
|
/// RET504
|
||||||
fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) {
|
fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) {
|
||||||
if let Expr::Name(ast::ExprName { id, .. }) = expr {
|
if let Expr::Name(ast::ExprName { id, .. }) = expr {
|
||||||
if !stack.assignments.contains_key(id.as_str()) {
|
if !stack.assigned_names.contains(id.as_str()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,9 +607,9 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_multiple_assigns(id, stack)
|
if has_multiple_declarations(id, stack)
|
||||||
|| has_refs_before_next_assign(id, expr.range(), stack)
|
|| has_references_before_next_declaration(id, expr.range(), stack)
|
||||||
|| has_refs_or_assigns_within_try_or_loop(id, stack)
|
|| has_references_or_declarations_within_try_or_loop(id, stack)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,14 @@ pub(crate) struct Stack<'a> {
|
||||||
pub(crate) yields: Vec<&'a Expr>,
|
pub(crate) yields: Vec<&'a Expr>,
|
||||||
pub(crate) elses: Vec<&'a Stmt>,
|
pub(crate) elses: Vec<&'a Stmt>,
|
||||||
pub(crate) elifs: Vec<&'a Stmt>,
|
pub(crate) elifs: Vec<&'a Stmt>,
|
||||||
|
/// The names that are assigned to in the current scope (e.g., anything on the left-hand side of
|
||||||
|
/// an assignment).
|
||||||
|
pub(crate) assigned_names: FxHashSet<&'a str>,
|
||||||
|
/// The names that are declared in the current scope, and the ranges of those declarations
|
||||||
|
/// (e.g., assignments, but also function and class definitions).
|
||||||
|
pub(crate) declarations: FxHashMap<&'a str, Vec<TextSize>>,
|
||||||
pub(crate) references: FxHashMap<&'a str, Vec<TextSize>>,
|
pub(crate) references: FxHashMap<&'a str, Vec<TextSize>>,
|
||||||
pub(crate) non_locals: FxHashSet<&'a str>,
|
pub(crate) non_locals: FxHashSet<&'a str>,
|
||||||
pub(crate) assignments: FxHashMap<&'a str, Vec<TextSize>>,
|
|
||||||
pub(crate) loops: Vec<TextRange>,
|
pub(crate) loops: Vec<TextRange>,
|
||||||
pub(crate) tries: Vec<TextRange>,
|
pub(crate) tries: Vec<TextRange>,
|
||||||
}
|
}
|
||||||
|
@ -34,8 +39,9 @@ impl<'a> ReturnVisitor<'a> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Expr::Name(ast::ExprName { id, .. }) => {
|
Expr::Name(ast::ExprName { id, .. }) => {
|
||||||
|
self.stack.assigned_names.insert(id.as_str());
|
||||||
self.stack
|
self.stack
|
||||||
.assignments
|
.declarations
|
||||||
.entry(id)
|
.entry(id)
|
||||||
.or_insert_with(Vec::new)
|
.or_insert_with(Vec::new)
|
||||||
.push(expr.start());
|
.push(expr.start());
|
||||||
|
@ -45,7 +51,7 @@ impl<'a> ReturnVisitor<'a> {
|
||||||
// Attribute assignments are often side-effects (e.g., `self.property = value`),
|
// Attribute assignments are often side-effects (e.g., `self.property = value`),
|
||||||
// so we conservatively treat them as references to every known
|
// so we conservatively treat them as references to every known
|
||||||
// variable.
|
// variable.
|
||||||
for name in self.stack.assignments.keys() {
|
for name in self.stack.declarations.keys() {
|
||||||
self.stack
|
self.stack
|
||||||
.references
|
.references
|
||||||
.entry(name)
|
.entry(name)
|
||||||
|
@ -68,18 +74,44 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||||
.non_locals
|
.non_locals
|
||||||
.extend(names.iter().map(Identifier::as_str));
|
.extend(names.iter().map(Identifier::as_str));
|
||||||
}
|
}
|
||||||
Stmt::FunctionDef(ast::StmtFunctionDef {
|
Stmt::ClassDef(ast::StmtClassDef {
|
||||||
decorator_list,
|
decorator_list,
|
||||||
|
name,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
// Mark a declaration.
|
||||||
|
self.stack
|
||||||
|
.declarations
|
||||||
|
.entry(name.as_str())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(stmt.start());
|
||||||
|
|
||||||
|
// Don't recurse into the body, but visit the decorators, etc.
|
||||||
|
for expr in decorator_list {
|
||||||
|
visitor::walk_expr(self, expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||||
|
name,
|
||||||
args,
|
args,
|
||||||
|
decorator_list,
|
||||||
returns,
|
returns,
|
||||||
..
|
..
|
||||||
})
|
})
|
||||||
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
|
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
|
||||||
decorator_list,
|
name,
|
||||||
args,
|
args,
|
||||||
|
decorator_list,
|
||||||
returns,
|
returns,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
|
// Mark a declaration.
|
||||||
|
self.stack
|
||||||
|
.declarations
|
||||||
|
.entry(name.as_str())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(stmt.start());
|
||||||
|
|
||||||
// Don't recurse into the body, but visit the decorators, etc.
|
// Don't recurse into the body, but visit the decorators, etc.
|
||||||
for expr in decorator_list {
|
for expr in decorator_list {
|
||||||
visitor::walk_expr(self, expr);
|
visitor::walk_expr(self, expr);
|
||||||
|
@ -138,7 +170,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||||
|
|
||||||
if let Some(target) = targets.first() {
|
if let Some(target) = targets.first() {
|
||||||
// Skip unpacking assignments, like `x, y = my_object`.
|
// Skip unpacking assignments, like `x, y = my_object`.
|
||||||
if matches!(target, Expr::Tuple(_)) && !value.is_tuple_expr() {
|
if target.is_tuple_expr() && !value.is_tuple_expr() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +204,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||||
Expr::Call(_) => {
|
Expr::Call(_) => {
|
||||||
// Arbitrary function calls can have side effects, so we conservatively treat
|
// Arbitrary function calls can have side effects, so we conservatively treat
|
||||||
// every function call as a reference to every known variable.
|
// every function call as a reference to every known variable.
|
||||||
for name in self.stack.assignments.keys() {
|
for name in self.stack.declarations.keys() {
|
||||||
self.stack
|
self.stack
|
||||||
.references
|
.references
|
||||||
.entry(name)
|
.entry(name)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue