mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:35 +00:00
Validate that mutable and immutable defaults are imported (#710)
This commit is contained in:
parent
b7acf76aaf
commit
00b5d1059c
4 changed files with 104 additions and 45 deletions
11
resources/test/fixtures/B008_extended.py
vendored
11
resources/test/fixtures/B008_extended.py
vendored
|
@ -4,14 +4,17 @@ import fastapi
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
|
|
||||||
|
|
||||||
def this_is_okay_extended(db=fastapi.Depends(get_db)):
|
def okay(db=fastapi.Depends(get_db)):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
def this_is_okay_extended_second(data: List[str] = fastapi.Query(None)):
|
def okay(data: List[str] = fastapi.Query(None)):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
# TODO(charlie): Support `import from`.
|
def okay(data: List[str] = Query(None)):
|
||||||
def this_is_not_okay_relative_import_not_listed(data: List[str] = Query(None)):
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def error_due_to_missing_import(data: List[str] = Depends(None)):
|
||||||
...
|
...
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use fnv::{FnvHashMap, FnvHashSet};
|
||||||
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
|
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
|
||||||
|
|
||||||
use crate::ast::helpers::compose_call_path;
|
use crate::ast::helpers::compose_call_path;
|
||||||
|
@ -8,31 +9,58 @@ use crate::check_ast::Checker;
|
||||||
use crate::checks::{Check, CheckKind};
|
use crate::checks::{Check, CheckKind};
|
||||||
use crate::flake8_bugbear::plugins::mutable_argument_default::is_mutable_func;
|
use crate::flake8_bugbear::plugins::mutable_argument_default::is_mutable_func;
|
||||||
|
|
||||||
// TODO(charlie): Verify imports for each of the imported members.
|
const IMMUTABLE_FUNCS: [&str; 7] = [
|
||||||
const IMMUTABLE_FUNCS: [&str; 11] = [
|
|
||||||
"tuple",
|
"tuple",
|
||||||
"frozenset",
|
"frozenset",
|
||||||
"operator.attrgetter",
|
"operator.attrgetter",
|
||||||
"operator.itemgetter",
|
"operator.itemgetter",
|
||||||
"operator.methodcaller",
|
"operator.methodcaller",
|
||||||
"attrgetter",
|
|
||||||
"itemgetter",
|
|
||||||
"methodcaller",
|
|
||||||
"types.MappingProxyType",
|
"types.MappingProxyType",
|
||||||
"MappingProxyType",
|
|
||||||
"re.compile",
|
"re.compile",
|
||||||
];
|
];
|
||||||
|
|
||||||
fn is_immutable_func(expr: &Expr, extend_immutable_calls: &[String]) -> bool {
|
fn is_immutable_func(
|
||||||
|
expr: &Expr,
|
||||||
|
extend_immutable_calls: &[&str],
|
||||||
|
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
|
||||||
|
) -> bool {
|
||||||
compose_call_path(expr).map_or_else(
|
compose_call_path(expr).map_or_else(
|
||||||
|| false,
|
|| false,
|
||||||
|func| IMMUTABLE_FUNCS.contains(&func.as_str()) || extend_immutable_calls.contains(&func),
|
|call_path| {
|
||||||
|
// It matches the call path exactly (`operator.methodcaller`).
|
||||||
|
for target in IMMUTABLE_FUNCS.iter().chain(extend_immutable_calls) {
|
||||||
|
if &call_path == target {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It matches the member name, and was imported from that module (`methodcaller`
|
||||||
|
// following `from operator import methodcaller`).
|
||||||
|
if !call_path.contains('.') {
|
||||||
|
for target in IMMUTABLE_FUNCS.iter().chain(extend_immutable_calls) {
|
||||||
|
let mut splitter = target.rsplit('.');
|
||||||
|
if let (Some(member), Some(module)) = (splitter.next(), splitter.next()) {
|
||||||
|
if call_path == member
|
||||||
|
&& from_imports
|
||||||
|
.get(module)
|
||||||
|
.map(|module| module.contains(member))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ArgumentDefaultVisitor<'a> {
|
struct ArgumentDefaultVisitor<'a> {
|
||||||
checks: Vec<(CheckKind, Range)>,
|
checks: Vec<(CheckKind, Range)>,
|
||||||
extend_immutable_calls: &'a [String],
|
extend_immutable_calls: &'a [&'a str],
|
||||||
|
from_imports: &'a FnvHashMap<&'a str, FnvHashSet<&'a str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
|
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
|
||||||
|
@ -42,8 +70,8 @@ where
|
||||||
fn visit_expr(&mut self, expr: &'b Expr) {
|
fn visit_expr(&mut self, expr: &'b Expr) {
|
||||||
match &expr.node {
|
match &expr.node {
|
||||||
ExprKind::Call { func, args, .. } => {
|
ExprKind::Call { func, args, .. } => {
|
||||||
if !is_mutable_func(func)
|
if !is_mutable_func(func, self.from_imports)
|
||||||
&& !is_immutable_func(func, self.extend_immutable_calls)
|
&& !is_immutable_func(func, self.extend_immutable_calls, self.from_imports)
|
||||||
&& !is_nan_or_infinity(func, args)
|
&& !is_nan_or_infinity(func, args)
|
||||||
{
|
{
|
||||||
self.checks.push((
|
self.checks.push((
|
||||||
|
@ -87,9 +115,17 @@ fn is_nan_or_infinity(expr: &Expr, args: &[Expr]) -> bool {
|
||||||
|
|
||||||
/// B008
|
/// B008
|
||||||
pub fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) {
|
pub fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) {
|
||||||
|
let extend_immutable_cells: Vec<&str> = checker
|
||||||
|
.settings
|
||||||
|
.flake8_bugbear
|
||||||
|
.extend_immutable_calls
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect();
|
||||||
let mut visitor = ArgumentDefaultVisitor {
|
let mut visitor = ArgumentDefaultVisitor {
|
||||||
checks: vec![],
|
checks: vec![],
|
||||||
extend_immutable_calls: &checker.settings.flake8_bugbear.extend_immutable_calls,
|
extend_immutable_calls: &extend_immutable_cells,
|
||||||
|
from_imports: &checker.from_imports,
|
||||||
};
|
};
|
||||||
for expr in arguments
|
for expr in arguments
|
||||||
.defaults
|
.defaults
|
||||||
|
|
|
@ -1,33 +1,53 @@
|
||||||
|
use crate::ast::helpers::compose_call_path;
|
||||||
|
use fnv::{FnvHashMap, FnvHashSet};
|
||||||
use rustpython_ast::{Arguments, Expr, ExprKind};
|
use rustpython_ast::{Arguments, Expr, ExprKind};
|
||||||
|
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::check_ast::Checker;
|
use crate::check_ast::Checker;
|
||||||
use crate::checks::{Check, CheckKind};
|
use crate::checks::{Check, CheckKind};
|
||||||
|
|
||||||
// TODO(charlie): Verify imports for each of the imported members.
|
const MUTABLE_FUNCS: [&str; 7] = [
|
||||||
pub fn is_mutable_func(expr: &Expr) -> bool {
|
"dict",
|
||||||
match &expr.node {
|
"list",
|
||||||
ExprKind::Name { id, .. }
|
"set",
|
||||||
if id == "dict"
|
"collections.Counter",
|
||||||
|| id == "list"
|
"collections.OrderedDict",
|
||||||
|| id == "set"
|
"collections.defaultdict",
|
||||||
|| id == "Counter"
|
"collections.deque",
|
||||||
|| id == "OrderedDict"
|
];
|
||||||
|| id == "defaultdict"
|
|
||||||
|| id == "deque" =>
|
pub fn is_mutable_func(expr: &Expr, from_imports: &FnvHashMap<&str, FnvHashSet<&str>>) -> bool {
|
||||||
{
|
compose_call_path(expr).map_or_else(
|
||||||
true
|
|| false,
|
||||||
}
|
|call_path| {
|
||||||
ExprKind::Attribute { value, attr, .. }
|
// It matches the call path exactly (`collections.Counter`).
|
||||||
if (attr == "Counter"
|
for target in MUTABLE_FUNCS {
|
||||||
|| attr == "OrderedDict"
|
if call_path == target {
|
||||||
|| attr == "defaultdict"
|
return true;
|
||||||
|| attr == "deque") =>
|
}
|
||||||
{
|
}
|
||||||
matches!(&value.node, ExprKind::Name { id, .. } if id == "collections")
|
|
||||||
}
|
// It matches the member name, and was imported from that module (`Counter`
|
||||||
_ => false,
|
// following `from collections import Counter`).
|
||||||
}
|
if !call_path.contains('.') {
|
||||||
|
for target in MUTABLE_FUNCS {
|
||||||
|
let mut splitter = target.rsplit('.');
|
||||||
|
if let (Some(member), Some(module)) = (splitter.next(), splitter.next()) {
|
||||||
|
if call_path == member
|
||||||
|
&& from_imports
|
||||||
|
.get(module)
|
||||||
|
.map(|module| module.contains(member))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// B006
|
/// B006
|
||||||
|
@ -50,7 +70,7 @@ pub fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
ExprKind::Call { func, .. } => {
|
ExprKind::Call { func, .. } => {
|
||||||
if is_mutable_func(func) {
|
if is_mutable_func(func, &checker.from_imports) {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::MutableArgumentDefault,
|
CheckKind::MutableArgumentDefault,
|
||||||
Range::from_located(expr),
|
Range::from_located(expr),
|
||||||
|
|
|
@ -4,10 +4,10 @@ expression: checks
|
||||||
---
|
---
|
||||||
- kind: FunctionCallArgumentDefault
|
- kind: FunctionCallArgumentDefault
|
||||||
location:
|
location:
|
||||||
row: 16
|
row: 19
|
||||||
column: 66
|
column: 50
|
||||||
end_location:
|
end_location:
|
||||||
row: 16
|
row: 19
|
||||||
column: 77
|
column: 63
|
||||||
fix: ~
|
fix: ~
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue