diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py new file mode 100644 index 0000000000000..e558cf05148c3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py @@ -0,0 +1,50 @@ +import math + + +### Safely fixable + +# Arguments are not checked +int(id()) +int(len([])) +int(ord(foo)) +int(hash(foo, bar)) +int(int('')) + +int(math.comb()) +int(math.factorial()) +int(math.gcd()) +int(math.lcm()) +int(math.isqrt()) +int(math.perm()) + + +### Unsafe + +int(math.ceil()) +int(math.floor()) +int(math.trunc()) + + +### `round()` + +## Errors +int(round(0)) +int(round(0, 0)) +int(round(0, None)) + +int(round(0.1)) +int(round(0.1, None)) + +# Argument type is not checked +foo = type("Foo", (), {"__round__": lambda self: 4.2})() + +int(round(foo)) +int(round(foo, 0)) +int(round(foo, None)) + +## No errors +int(round(0, 3.14)) +int(round(0, non_literal)) +int(round(0, 0), base) +int(round(0, 0, extra=keyword)) +int(round(0.1, 0)) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index ae3dfdc31b859..d582aaef64746 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1093,6 +1093,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::Airflow3Removal) { airflow::rules::removed_in_3(checker, expr); } + if checker.enabled(Rule::UnnecessaryCastToInt) { + ruff::rules::unnecessary_cast_to_int(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index d35bf82bbde2e..5b89fe84fdbda 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -983,6 +983,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern), (Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument), (Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral), + (Ruff, "046") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryCastToInt), (Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing), (Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable), (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 14d6f3e121a35..69a3ea3053b80 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -413,6 +413,7 @@ mod tests { #[test_case(Rule::UnrawRePattern, Path::new("RUF039_concat.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_0.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))] + #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 69f92b8da2bab..ac0a17445cc49 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -30,6 +30,7 @@ pub(crate) use sort_dunder_slots::*; pub(crate) use static_key_dict_comprehension::*; #[cfg(any(feature = "test-rules", test))] pub(crate) use test_rules::*; +pub(crate) use unnecessary_cast_to_int::*; pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_key_check::*; pub(crate) use unnecessary_nested_literal::*; @@ -78,6 +79,7 @@ mod static_key_dict_comprehension; mod suppression_comment_visitor; #[cfg(any(feature = "test-rules", test))] pub(crate) mod test_rules; +mod unnecessary_cast_to_int; mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_key_check; mod unnecessary_nested_literal; diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs new file mode 100644 index 0000000000000..ddbe26a24ae47 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -0,0 +1,183 @@ +use crate::checkers::ast::Checker; +use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::{Arguments, Expr, ExprCall, ExprName, ExprNumberLiteral, Number}; +use ruff_python_semantic::analyze::typing; +use ruff_python_semantic::SemanticModel; +use ruff_text_size::TextRange; + +/// ## What it does +/// Checks for `int` conversions of values that are already integers. +/// +/// ## Why is this bad? +/// Such a conversion is unnecessary. +/// +/// ## Known problems +/// This rule may produce false positives for `round`, `math.ceil`, `math.floor`, +/// and `math.trunc` calls when values override the `__round__`, `__ceil__`, `__floor__`, +/// or `__trunc__` operators such that they don't return an integer. +/// +/// ## Example +/// +/// ```python +/// int(len([])) +/// int(round(foo, None)) +/// ``` +/// +/// Use instead: +/// +/// ```python +/// len([]) +/// round(foo) +/// ``` +/// +/// ## Fix safety +/// The fix for `round`, `math.ceil`, `math.floor`, and `math.truncate` is unsafe +/// because removing the `int` conversion can change the semantics for values +/// overriding the `__round__`, `__ceil__`, `__floor__`, or `__trunc__` dunder methods +/// such that they don't return an integer. +#[derive(ViolationMetadata)] +pub(crate) struct UnnecessaryCastToInt; + +impl AlwaysFixableViolation for UnnecessaryCastToInt { + #[derive_message_formats] + fn message(&self) -> String { + "Value being casted is already an integer".to_string() + } + + fn fix_title(&self) -> String { + "Remove unnecessary conversion to `int`".to_string() + } +} + +/// RUF046 +pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { + let semantic = checker.semantic(); + + let Some(Expr::Call(inner_call)) = single_argument_to_int_call(semantic, call) else { + return; + }; + + let (func, arguments) = (&inner_call.func, &inner_call.arguments); + let (outer_range, inner_range) = (call.range, inner_call.range); + + let Some(qualified_name) = checker.semantic().resolve_qualified_name(func) else { + return; + }; + + let fix = match qualified_name.segments() { + // Always returns a strict instance of `int` + ["" | "builtins", "len" | "id" | "hash" | "ord" | "int"] + | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] => { + Fix::safe_edit(replace_with_inner(checker, outer_range, inner_range)) + } + + // Depends on `ndigits` and `number.__round__` + ["" | "builtins", "round"] => { + if let Some(fix) = replace_with_shortened_round_call(checker, outer_range, arguments) { + fix + } else { + return; + } + } + + // Depends on `__ceil__`/`__floor__`/`__trunc__` + ["math", "ceil" | "floor" | "trunc"] => { + Fix::unsafe_edit(replace_with_inner(checker, outer_range, inner_range)) + } + + _ => return, + }; + + checker + .diagnostics + .push(Diagnostic::new(UnnecessaryCastToInt, call.range).with_fix(fix)); +} + +fn single_argument_to_int_call<'a>( + semantic: &SemanticModel, + call: &'a ExprCall, +) -> Option<&'a Expr> { + let ExprCall { + func, arguments, .. + } = call; + + if !semantic.match_builtin_expr(func, "int") { + return None; + } + + if !arguments.keywords.is_empty() { + return None; + } + + let [argument] = &*arguments.args else { + return None; + }; + + Some(argument) +} + +/// Returns an [`Edit`] when the call is of any of the forms: +/// * `round(integer)`, `round(integer, 0)`, `round(integer, None)` +/// * `round(whatever)`, `round(whatever, None)` +fn replace_with_shortened_round_call( + checker: &Checker, + outer_range: TextRange, + arguments: &Arguments, +) -> Option { + if arguments.len() > 2 { + return None; + } + + let number = arguments.find_argument("number", 0)?; + let ndigits = arguments.find_argument("ndigits", 1); + + let number_is_int = match number { + Expr::Name(name) => is_int(checker.semantic(), name), + Expr::NumberLiteral(ExprNumberLiteral { value, .. }) => matches!(value, Number::Int(..)), + _ => false, + }; + + match ndigits { + Some(Expr::NumberLiteral(ExprNumberLiteral { value, .. })) + if is_literal_zero(value) && number_is_int => {} + Some(Expr::NoneLiteral(_)) | None => {} + _ => return None, + }; + + let number_expr = checker.locator().slice(number); + let new_content = format!("round({number_expr})"); + + let applicability = if number_is_int { + Applicability::Safe + } else { + Applicability::Unsafe + }; + + Some(Fix::applicable_edit( + Edit::range_replacement(new_content, outer_range), + applicability, + )) +} + +fn is_int(semantic: &SemanticModel, name: &ExprName) -> bool { + let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { + return false; + }; + + typing::is_int(binding, semantic) +} + +fn is_literal_zero(value: &Number) -> bool { + let Number::Int(int) = value else { + return false; + }; + + matches!(int.as_u8(), Some(0)) +} + +fn replace_with_inner(checker: &Checker, outer_range: TextRange, inner_range: TextRange) -> Edit { + let inner_expr = checker.locator().slice(inner_range); + + Edit::range_replacement(inner_expr.to_string(), outer_range) +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap new file mode 100644 index 0000000000000..22e139816bcf8 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -0,0 +1,430 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF046.py:7:1: RUF046 [*] Value being casted is already an integer + | +6 | # Arguments are not checked +7 | int(id()) + | ^^^^^^^^^ RUF046 +8 | int(len([])) +9 | int(ord(foo)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +4 4 | ### Safely fixable +5 5 | +6 6 | # Arguments are not checked +7 |-int(id()) + 7 |+id() +8 8 | int(len([])) +9 9 | int(ord(foo)) +10 10 | int(hash(foo, bar)) + +RUF046.py:8:1: RUF046 [*] Value being casted is already an integer + | + 6 | # Arguments are not checked + 7 | int(id()) + 8 | int(len([])) + | ^^^^^^^^^^^^ RUF046 + 9 | int(ord(foo)) +10 | int(hash(foo, bar)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +5 5 | +6 6 | # Arguments are not checked +7 7 | int(id()) +8 |-int(len([])) + 8 |+len([]) +9 9 | int(ord(foo)) +10 10 | int(hash(foo, bar)) +11 11 | int(int('')) + +RUF046.py:9:1: RUF046 [*] Value being casted is already an integer + | + 7 | int(id()) + 8 | int(len([])) + 9 | int(ord(foo)) + | ^^^^^^^^^^^^^ RUF046 +10 | int(hash(foo, bar)) +11 | int(int('')) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +6 6 | # Arguments are not checked +7 7 | int(id()) +8 8 | int(len([])) +9 |-int(ord(foo)) + 9 |+ord(foo) +10 10 | int(hash(foo, bar)) +11 11 | int(int('')) +12 12 | + +RUF046.py:10:1: RUF046 [*] Value being casted is already an integer + | + 8 | int(len([])) + 9 | int(ord(foo)) +10 | int(hash(foo, bar)) + | ^^^^^^^^^^^^^^^^^^^ RUF046 +11 | int(int('')) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +7 7 | int(id()) +8 8 | int(len([])) +9 9 | int(ord(foo)) +10 |-int(hash(foo, bar)) + 10 |+hash(foo, bar) +11 11 | int(int('')) +12 12 | +13 13 | int(math.comb()) + +RUF046.py:11:1: RUF046 [*] Value being casted is already an integer + | + 9 | int(ord(foo)) +10 | int(hash(foo, bar)) +11 | int(int('')) + | ^^^^^^^^^^^^ RUF046 +12 | +13 | int(math.comb()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +8 8 | int(len([])) +9 9 | int(ord(foo)) +10 10 | int(hash(foo, bar)) +11 |-int(int('')) + 11 |+int('') +12 12 | +13 13 | int(math.comb()) +14 14 | int(math.factorial()) + +RUF046.py:13:1: RUF046 [*] Value being casted is already an integer + | +11 | int(int('')) +12 | +13 | int(math.comb()) + | ^^^^^^^^^^^^^^^^ RUF046 +14 | int(math.factorial()) +15 | int(math.gcd()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +10 10 | int(hash(foo, bar)) +11 11 | int(int('')) +12 12 | +13 |-int(math.comb()) + 13 |+math.comb() +14 14 | int(math.factorial()) +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) + +RUF046.py:14:1: RUF046 [*] Value being casted is already an integer + | +13 | int(math.comb()) +14 | int(math.factorial()) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +15 | int(math.gcd()) +16 | int(math.lcm()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +11 11 | int(int('')) +12 12 | +13 13 | int(math.comb()) +14 |-int(math.factorial()) + 14 |+math.factorial() +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) +17 17 | int(math.isqrt()) + +RUF046.py:15:1: RUF046 [*] Value being casted is already an integer + | +13 | int(math.comb()) +14 | int(math.factorial()) +15 | int(math.gcd()) + | ^^^^^^^^^^^^^^^ RUF046 +16 | int(math.lcm()) +17 | int(math.isqrt()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +12 12 | +13 13 | int(math.comb()) +14 14 | int(math.factorial()) +15 |-int(math.gcd()) + 15 |+math.gcd() +16 16 | int(math.lcm()) +17 17 | int(math.isqrt()) +18 18 | int(math.perm()) + +RUF046.py:16:1: RUF046 [*] Value being casted is already an integer + | +14 | int(math.factorial()) +15 | int(math.gcd()) +16 | int(math.lcm()) + | ^^^^^^^^^^^^^^^ RUF046 +17 | int(math.isqrt()) +18 | int(math.perm()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +13 13 | int(math.comb()) +14 14 | int(math.factorial()) +15 15 | int(math.gcd()) +16 |-int(math.lcm()) + 16 |+math.lcm() +17 17 | int(math.isqrt()) +18 18 | int(math.perm()) +19 19 | + +RUF046.py:17:1: RUF046 [*] Value being casted is already an integer + | +15 | int(math.gcd()) +16 | int(math.lcm()) +17 | int(math.isqrt()) + | ^^^^^^^^^^^^^^^^^ RUF046 +18 | int(math.perm()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +14 14 | int(math.factorial()) +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) +17 |-int(math.isqrt()) + 17 |+math.isqrt() +18 18 | int(math.perm()) +19 19 | +20 20 | + +RUF046.py:18:1: RUF046 [*] Value being casted is already an integer + | +16 | int(math.lcm()) +17 | int(math.isqrt()) +18 | int(math.perm()) + | ^^^^^^^^^^^^^^^^ RUF046 + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) +17 17 | int(math.isqrt()) +18 |-int(math.perm()) + 18 |+math.perm() +19 19 | +20 20 | +21 21 | ### Unsafe + +RUF046.py:23:1: RUF046 [*] Value being casted is already an integer + | +21 | ### Unsafe +22 | +23 | int(math.ceil()) + | ^^^^^^^^^^^^^^^^ RUF046 +24 | int(math.floor()) +25 | int(math.trunc()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +20 20 | +21 21 | ### Unsafe +22 22 | +23 |-int(math.ceil()) + 23 |+math.ceil() +24 24 | int(math.floor()) +25 25 | int(math.trunc()) +26 26 | + +RUF046.py:24:1: RUF046 [*] Value being casted is already an integer + | +23 | int(math.ceil()) +24 | int(math.floor()) + | ^^^^^^^^^^^^^^^^^ RUF046 +25 | int(math.trunc()) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +21 21 | ### Unsafe +22 22 | +23 23 | int(math.ceil()) +24 |-int(math.floor()) + 24 |+math.floor() +25 25 | int(math.trunc()) +26 26 | +27 27 | + +RUF046.py:25:1: RUF046 [*] Value being casted is already an integer + | +23 | int(math.ceil()) +24 | int(math.floor()) +25 | int(math.trunc()) + | ^^^^^^^^^^^^^^^^^ RUF046 + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +22 22 | +23 23 | int(math.ceil()) +24 24 | int(math.floor()) +25 |-int(math.trunc()) + 25 |+math.trunc() +26 26 | +27 27 | +28 28 | ### `round()` + +RUF046.py:31:1: RUF046 [*] Value being casted is already an integer + | +30 | ## Errors +31 | int(round(0)) + | ^^^^^^^^^^^^^ RUF046 +32 | int(round(0, 0)) +33 | int(round(0, None)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +28 28 | ### `round()` +29 29 | +30 30 | ## Errors +31 |-int(round(0)) + 31 |+round(0) +32 32 | int(round(0, 0)) +33 33 | int(round(0, None)) +34 34 | + +RUF046.py:32:1: RUF046 [*] Value being casted is already an integer + | +30 | ## Errors +31 | int(round(0)) +32 | int(round(0, 0)) + | ^^^^^^^^^^^^^^^^ RUF046 +33 | int(round(0, None)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +29 29 | +30 30 | ## Errors +31 31 | int(round(0)) +32 |-int(round(0, 0)) + 32 |+round(0) +33 33 | int(round(0, None)) +34 34 | +35 35 | int(round(0.1)) + +RUF046.py:33:1: RUF046 [*] Value being casted is already an integer + | +31 | int(round(0)) +32 | int(round(0, 0)) +33 | int(round(0, None)) + | ^^^^^^^^^^^^^^^^^^^ RUF046 +34 | +35 | int(round(0.1)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Safe fix +30 30 | ## Errors +31 31 | int(round(0)) +32 32 | int(round(0, 0)) +33 |-int(round(0, None)) + 33 |+round(0) +34 34 | +35 35 | int(round(0.1)) +36 36 | int(round(0.1, None)) + +RUF046.py:35:1: RUF046 [*] Value being casted is already an integer + | +33 | int(round(0, None)) +34 | +35 | int(round(0.1)) + | ^^^^^^^^^^^^^^^ RUF046 +36 | int(round(0.1, None)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +32 32 | int(round(0, 0)) +33 33 | int(round(0, None)) +34 34 | +35 |-int(round(0.1)) + 35 |+round(0.1) +36 36 | int(round(0.1, None)) +37 37 | +38 38 | # Argument type is not checked + +RUF046.py:36:1: RUF046 [*] Value being casted is already an integer + | +35 | int(round(0.1)) +36 | int(round(0.1, None)) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +37 | +38 | # Argument type is not checked + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +33 33 | int(round(0, None)) +34 34 | +35 35 | int(round(0.1)) +36 |-int(round(0.1, None)) + 36 |+round(0.1) +37 37 | +38 38 | # Argument type is not checked +39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() + +RUF046.py:41:1: RUF046 [*] Value being casted is already an integer + | +39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +40 | +41 | int(round(foo)) + | ^^^^^^^^^^^^^^^ RUF046 +42 | int(round(foo, 0)) +43 | int(round(foo, None)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +38 38 | # Argument type is not checked +39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +40 40 | +41 |-int(round(foo)) + 41 |+round(foo) +42 42 | int(round(foo, 0)) +43 43 | int(round(foo, None)) +44 44 | + +RUF046.py:43:1: RUF046 [*] Value being casted is already an integer + | +41 | int(round(foo)) +42 | int(round(foo, 0)) +43 | int(round(foo, None)) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +44 | +45 | ## No errors + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +40 40 | +41 41 | int(round(foo)) +42 42 | int(round(foo, 0)) +43 |-int(round(foo, None)) + 43 |+round(foo) +44 44 | +45 45 | ## No errors +46 46 | int(round(0, 3.14)) diff --git a/crates/ruff_python_ast/src/int.rs b/crates/ruff_python_ast/src/int.rs index bbcf1b0b2a349..4d918f5574354 100644 --- a/crates/ruff_python_ast/src/int.rs +++ b/crates/ruff_python_ast/src/int.rs @@ -96,7 +96,7 @@ impl Int { } } - /// Return the [`Int`] as an u64, if it can be represented as that data type. + /// Return the [`Int`] as an usize, if it can be represented as that data type. pub fn as_usize(&self) -> Option { match &self.0 { Number::Small(small) => usize::try_from(*small).ok(), diff --git a/ruff.schema.json b/ruff.schema.json index 72ff32f77bace..4ababbdf9eb30 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3843,6 +3843,7 @@ "RUF04", "RUF040", "RUF041", + "RUF046", "RUF048", "RUF05", "RUF052",