Allow more immutable funcs for RUF009 (#4660)

This commit is contained in:
qdegraaf 2023-05-26 21:18:52 +02:00 committed by GitHub
parent 12e45498e8
commit ccca11839a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 155 additions and 173 deletions

View file

@ -1,6 +1,7 @@
import collections import collections
import datetime as dt import datetime as dt
from decimal import Decimal from decimal import Decimal
from fractions import Fraction
import logging import logging
import operator import operator
from pathlib import Path from pathlib import Path
@ -158,12 +159,37 @@ def float_infinity_literal(value=float("1e999")):
pass pass
# But don't allow standard floats # Allow standard floats
def float_int_is_wrong(value=float(3)): def float_int_okay(value=float(3)):
pass pass
def float_str_not_inf_or_nan_is_wrong(value=float("3.14")): def float_str_not_inf_or_nan_okay(value=float("3.14")):
pass
# Allow immutable str() value
def str_okay(value=str("foo")):
pass
# Allow immutable bool() value
def bool_okay(value=bool("bar")):
pass
# Allow immutable int() value
def int_okay(value=int("12")):
pass
# Allow immutable complex() value
def complex_okay(value=complex(1,2)):
pass
# Allow immutable Fraction() value
def fraction_okay(value=Fraction(1,2)):
pass pass

View file

@ -2,10 +2,10 @@ import datetime
import re import re
import typing import typing
from dataclasses import dataclass, field from dataclasses import dataclass, field
from fractions import Fraction
from pathlib import Path from pathlib import Path
from typing import ClassVar, NamedTuple from typing import ClassVar, NamedTuple
def default_function() -> list[int]: def default_function() -> list[int]:
return [] return []
@ -25,7 +25,12 @@ class A:
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7) fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
fine_tuple: tuple[int] = tuple([1]) fine_tuple: tuple[int] = tuple([1])
fine_regex: re.Pattern = re.compile(r".*") fine_regex: re.Pattern = re.compile(r".*")
fine_float: float = float('-inf')
fine_int: int = int(12)
fine_complex: complex = complex(1, 2)
fine_str: str = str("foo")
fine_bool: bool = bool("foo")
fine_fraction: Fraction = Fraction(1,2)
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3]) DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3])

View file

@ -1,5 +1,5 @@
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, Arguments, Constant, Expr, Ranged}; use rustpython_parser::ast::{self, Arguments, Expr, Ranged};
use ruff_diagnostics::Violation; use ruff_diagnostics::Violation;
use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_diagnostics::{Diagnostic, DiagnosticKind};
@ -94,10 +94,9 @@ where
{ {
fn visit_expr(&mut self, expr: &'b Expr) { fn visit_expr(&mut self, expr: &'b Expr) {
match expr { match expr {
Expr::Call(ast::ExprCall { func, args, .. }) => { Expr::Call(ast::ExprCall { func, .. }) => {
if !is_mutable_func(self.model, func) if !is_mutable_func(self.model, func)
&& !is_immutable_func(self.model, func, &self.extend_immutable_calls) && !is_immutable_func(self.model, func, &self.extend_immutable_calls)
&& !is_nan_or_infinity(func, args)
{ {
self.diagnostics.push(( self.diagnostics.push((
FunctionCallInDefaultArgument { FunctionCallInDefaultArgument {
@ -115,29 +114,6 @@ where
} }
} }
fn is_nan_or_infinity(expr: &Expr, args: &[Expr]) -> bool {
let Expr::Name(ast::ExprName { id, .. }) = expr else {
return false;
};
if id != "float" {
return false;
}
let Some(arg) = args.first() else {
return false;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
} )= arg else {
return false;
};
let lowercased = value.to_lowercase();
matches!(
lowercased.as_str(),
"nan" | "+nan" | "-nan" | "inf" | "+inf" | "-inf" | "infinity" | "+infinity" | "-infinity"
)
}
/// B008 /// B008
pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) { pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) {
// Map immutable calls to (module, member) format. // Map immutable calls to (module, member) format.

View file

@ -1,113 +1,113 @@
--- ---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs source: crates/ruff/src/rules/flake8_bugbear/mod.rs
--- ---
B006_B008.py:62:25: B006 Do not use mutable data structures for argument defaults B006_B008.py:63:25: B006 Do not use mutable data structures for argument defaults
| |
62 | def this_is_wrong(value=[1, 2, 3]): 63 | def this_is_wrong(value=[1, 2, 3]):
| ^^^^^^^^^ B006 | ^^^^^^^^^ B006
63 | ... 64 | ...
| |
B006_B008.py:66:30: B006 Do not use mutable data structures for argument defaults B006_B008.py:67:30: B006 Do not use mutable data structures for argument defaults
| |
66 | def this_is_also_wrong(value={}): 67 | def this_is_also_wrong(value={}):
| ^^ B006 | ^^ B006
67 | ... 68 | ...
| |
B006_B008.py:70:20: B006 Do not use mutable data structures for argument defaults B006_B008.py:71:20: B006 Do not use mutable data structures for argument defaults
| |
70 | def and_this(value=set()): 71 | def and_this(value=set()):
| ^^^^^ B006 | ^^^^^ B006
71 | ... 72 | ...
| |
B006_B008.py:74:20: B006 Do not use mutable data structures for argument defaults B006_B008.py:75:20: B006 Do not use mutable data structures for argument defaults
| |
74 | def this_too(value=collections.OrderedDict()): 75 | def this_too(value=collections.OrderedDict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006 | ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
75 | ... 76 | ...
| |
B006_B008.py:78:32: B006 Do not use mutable data structures for argument defaults B006_B008.py:79:32: B006 Do not use mutable data structures for argument defaults
| |
78 | async def async_this_too(value=collections.defaultdict()): 79 | async def async_this_too(value=collections.defaultdict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006 | ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
79 | ... 80 | ...
| |
B006_B008.py:82:26: B006 Do not use mutable data structures for argument defaults B006_B008.py:83:26: B006 Do not use mutable data structures for argument defaults
| |
82 | def dont_forget_me(value=collections.deque()): 83 | def dont_forget_me(value=collections.deque()):
| ^^^^^^^^^^^^^^^^^^^ B006 | ^^^^^^^^^^^^^^^^^^^ B006
83 | ... 84 | ...
| |
B006_B008.py:87:46: B006 Do not use mutable data structures for argument defaults B006_B008.py:88:46: B006 Do not use mutable data structures for argument defaults
| |
87 | # N.B. we're also flagging the function call in the comprehension 88 | # N.B. we're also flagging the function call in the comprehension
88 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): 89 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006 | ^^^^^^^^^^^^^^^^^^^^^^^^ B006
89 | pass 90 | pass
| |
B006_B008.py:91:46: B006 Do not use mutable data structures for argument defaults B006_B008.py:92:46: B006 Do not use mutable data structures for argument defaults
| |
91 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}): 92 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
92 | pass 93 | pass
| |
B006_B008.py:95:45: B006 Do not use mutable data structures for argument defaults B006_B008.py:96:45: B006 Do not use mutable data structures for argument defaults
| |
95 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}): 96 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006 | ^^^^^^^^^^^^^^^^^^^^^^^^ B006
96 | pass 97 | pass
| |
B006_B008.py:99:33: B006 Do not use mutable data structures for argument defaults B006_B008.py:100:33: B006 Do not use mutable data structures for argument defaults
| |
99 | def kwonlyargs_mutable(*, value=[]): 100 | def kwonlyargs_mutable(*, value=[]):
| ^^ B006 | ^^ B006
100 | ... 101 | ...
| |
B006_B008.py:192:20: B006 Do not use mutable data structures for argument defaults B006_B008.py:218:20: B006 Do not use mutable data structures for argument defaults
| |
192 | # B006 and B008 218 | # B006 and B008
193 | # We should handle arbitrary nesting of these B008. 219 | # We should handle arbitrary nesting of these B008.
194 | def nested_combo(a=[float(3), dt.datetime.now()]): 220 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
195 | pass 221 | pass
| |
B006_B008.py:225:27: B006 Do not use mutable data structures for argument defaults B006_B008.py:251:27: B006 Do not use mutable data structures for argument defaults
| |
225 | def mutable_annotations( 251 | def mutable_annotations(
226 | a: list[int] | None = [], 252 | a: list[int] | None = [],
| ^^ B006 | ^^ B006
227 | b: Optional[Dict[int, int]] = {}, 253 | b: Optional[Dict[int, int]] = {},
228 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 254 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| |
B006_B008.py:226:35: B006 Do not use mutable data structures for argument defaults B006_B008.py:252:35: B006 Do not use mutable data structures for argument defaults
| |
226 | def mutable_annotations( 252 | def mutable_annotations(
227 | a: list[int] | None = [], 253 | a: list[int] | None = [],
228 | b: Optional[Dict[int, int]] = {}, 254 | b: Optional[Dict[int, int]] = {},
| ^^ B006 | ^^ B006
229 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 255 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
230 | ): 256 | ):
| |
B006_B008.py:227:62: B006 Do not use mutable data structures for argument defaults B006_B008.py:253:62: B006 Do not use mutable data structures for argument defaults
| |
227 | a: list[int] | None = [], 253 | a: list[int] | None = [],
228 | b: Optional[Dict[int, int]] = {}, 254 | b: Optional[Dict[int, int]] = {},
229 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 255 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006 | ^^^^^ B006
230 | ): 256 | ):
231 | pass 257 | pass
| |

View file

@ -1,114 +1,83 @@
--- ---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs source: crates/ruff/src/rules/flake8_bugbear/mod.rs
--- ---
B006_B008.py:87:61: B008 Do not perform function call `range` in argument defaults B006_B008.py:88:61: B008 Do not perform function call `range` in argument defaults
| |
87 | # N.B. we're also flagging the function call in the comprehension 88 | # N.B. we're also flagging the function call in the comprehension
88 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): 89 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^ B008 | ^^^^^^^^ B008
89 | pass 90 | pass
| |
B006_B008.py:91:64: B008 Do not perform function call `range` in argument defaults B006_B008.py:92:64: B008 Do not perform function call `range` in argument defaults
| |
91 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}): 92 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^ B008 | ^^^^^^^^ B008
92 | pass 93 | pass
| |
B006_B008.py:95:60: B008 Do not perform function call `range` in argument defaults B006_B008.py:96:60: B008 Do not perform function call `range` in argument defaults
| |
95 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}): 96 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^ B008 | ^^^^^^^^ B008
96 | pass 97 | pass
| |
B006_B008.py:111:39: B008 Do not perform function call `time.time` in argument defaults B006_B008.py:112:39: B008 Do not perform function call `time.time` in argument defaults
| |
111 | # B008 112 | # B008
112 | # Flag function calls as default args (including if they are part of a sub-expression) 113 | # Flag function calls as default args (including if they are part of a sub-expression)
113 | def in_fact_all_calls_are_wrong(value=time.time()): 114 | def in_fact_all_calls_are_wrong(value=time.time()):
| ^^^^^^^^^^^ B008 | ^^^^^^^^^^^ B008
114 | ... 115 | ...
| |
B006_B008.py:115:12: B008 Do not perform function call `dt.datetime.now` in argument defaults B006_B008.py:116:12: B008 Do not perform function call `dt.datetime.now` in argument defaults
| |
115 | def f(when=dt.datetime.now() + dt.timedelta(days=7)): 116 | def f(when=dt.datetime.now() + dt.timedelta(days=7)):
| ^^^^^^^^^^^^^^^^^ B008 | ^^^^^^^^^^^^^^^^^ B008
116 | pass 117 | pass
| |
B006_B008.py:119:30: B008 Do not perform function call in argument defaults B006_B008.py:120:30: B008 Do not perform function call in argument defaults
| |
119 | def can_even_catch_lambdas(a=(lambda x: x)()): 120 | def can_even_catch_lambdas(a=(lambda x: x)()):
| ^^^^^^^^^^^^^^^ B008 | ^^^^^^^^^^^^^^^ B008
120 | ... 121 | ...
| |
B006_B008.py:157:34: B008 Do not perform function call `float` in argument defaults B006_B008.py:218:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
| |
157 | def float_infinity_literal(value=float("1e999")): 218 | # B006 and B008
| ^^^^^^^^^^^^^^ B008 219 | # We should handle arbitrary nesting of these B008.
158 | pass 220 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
B006_B008.py:162:30: B008 Do not perform function call `float` in argument defaults
|
162 | # But don't allow standard floats
163 | def float_int_is_wrong(value=float(3)):
| ^^^^^^^^ B008
164 | pass
|
B006_B008.py:166:45: B008 Do not perform function call `float` in argument defaults
|
166 | def float_str_not_inf_or_nan_is_wrong(value=float("3.14")):
| ^^^^^^^^^^^^^ B008
167 | pass
|
B006_B008.py:192:21: B008 Do not perform function call `float` in argument defaults
|
192 | # B006 and B008
193 | # We should handle arbitrary nesting of these B008.
194 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^ B008
195 | pass
|
B006_B008.py:192:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
192 | # B006 and B008
193 | # We should handle arbitrary nesting of these B008.
194 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ B008 | ^^^^^^^^^^^^^^^^^ B008
195 | pass 221 | pass
| |
B006_B008.py:198:22: B008 Do not perform function call `map` in argument defaults B006_B008.py:224:22: B008 Do not perform function call `map` in argument defaults
| |
198 | # Don't flag nested B006 since we can't guarantee that 224 | # Don't flag nested B006 since we can't guarantee that
199 | # it isn't made mutable by the outer operation. 225 | # it isn't made mutable by the outer operation.
200 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])): 226 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
201 | pass 227 | pass
| |
B006_B008.py:203:19: B008 Do not perform function call `random.randint` in argument defaults B006_B008.py:229:19: B008 Do not perform function call `random.randint` in argument defaults
| |
203 | # B008-ception. 229 | # B008-ception.
204 | def nested_b008(a=random.randint(0, dt.datetime.now().year)): 230 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
205 | pass 231 | pass
| |
B006_B008.py:203:37: B008 Do not perform function call `dt.datetime.now` in argument defaults B006_B008.py:229:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
| |
203 | # B008-ception. 229 | # B008-ception.
204 | def nested_b008(a=random.randint(0, dt.datetime.now().year)): 230 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ B008 | ^^^^^^^^^^^^^^^^^ B008
205 | pass 231 | pass
| |

View file

@ -11,34 +11,34 @@ RUF009.py:19:41: RUF009 Do not perform function call `default_function` in datac
23 | another_class_var: ClassVar[list[int]] = default_function() 23 | another_class_var: ClassVar[list[int]] = default_function()
| |
RUF009.py:36:41: RUF009 Do not perform function call `default_function` in dataclass defaults RUF009.py:41:41: RUF009 Do not perform function call `default_function` in dataclass defaults
| |
36 | @dataclass 41 | @dataclass
37 | class B: 42 | class B:
38 | hidden_mutable_default: list[int] = default_function() 43 | hidden_mutable_default: list[int] = default_function()
| ^^^^^^^^^^^^^^^^^^ RUF009 | ^^^^^^^^^^^^^^^^^^ RUF009
39 | another_dataclass: A = A() 44 | another_dataclass: A = A()
40 | not_optimal: ImmutableType = ImmutableType(20) 45 | not_optimal: ImmutableType = ImmutableType(20)
| |
RUF009.py:37:28: RUF009 Do not perform function call `A` in dataclass defaults RUF009.py:42:28: RUF009 Do not perform function call `A` in dataclass defaults
| |
37 | class B: 42 | class B:
38 | hidden_mutable_default: list[int] = default_function() 43 | hidden_mutable_default: list[int] = default_function()
39 | another_dataclass: A = A() 44 | another_dataclass: A = A()
| ^^^ RUF009 | ^^^ RUF009
40 | not_optimal: ImmutableType = ImmutableType(20) 45 | not_optimal: ImmutableType = ImmutableType(20)
41 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES 46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
| |
RUF009.py:38:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults RUF009.py:43:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults
| |
38 | hidden_mutable_default: list[int] = default_function() 43 | hidden_mutable_default: list[int] = default_function()
39 | another_dataclass: A = A() 44 | another_dataclass: A = A()
40 | not_optimal: ImmutableType = ImmutableType(20) 45 | not_optimal: ImmutableType = ImmutableType(20)
| ^^^^^^^^^^^^^^^^^ RUF009 | ^^^^^^^^^^^^^^^^^ RUF009
41 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES 46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
42 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES 47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
| |

View file

@ -201,12 +201,18 @@ pub fn is_immutable_annotation(model: &SemanticModel, expr: &Expr) -> bool {
} }
const IMMUTABLE_FUNCS: &[&[&str]] = &[ const IMMUTABLE_FUNCS: &[&[&str]] = &[
&["", "tuple"], &["", "bool"],
&["", "complex"],
&["", "float"],
&["", "frozenset"], &["", "frozenset"],
&["", "int"],
&["", "str"],
&["", "tuple"],
&["datetime", "date"], &["datetime", "date"],
&["datetime", "datetime"], &["datetime", "datetime"],
&["datetime", "timedelta"], &["datetime", "timedelta"],
&["decimal", "Decimal"], &["decimal", "Decimal"],
&["fractions", "Fraction"],
&["operator", "attrgetter"], &["operator", "attrgetter"],
&["operator", "itemgetter"], &["operator", "itemgetter"],
&["operator", "methodcaller"], &["operator", "methodcaller"],