[`refurb`] Expand fix safety for keyword arguments and `Decimal`s (`FURB164`) (#21259)

## Summary

Fixes https://github.com/astral-sh/ruff/issues/21257

## Test Plan

`cargo nextest run furb164`
This commit is contained in:
chiri 2025-11-04 00:09:02 +03:00 committed by GitHub
parent d2fe6347fb
commit 79a02711c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 20 deletions

View File

@ -64,3 +64,8 @@ _ = Decimal.from_float(True)
_ = Decimal.from_float(float("-nan")) _ = Decimal.from_float(float("-nan"))
_ = Decimal.from_float(float("\x2dnan")) _ = Decimal.from_float(float("\x2dnan"))
_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan")) _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
# See: https://github.com/astral-sh/ruff/issues/21257
# fixes must be safe
_ = Fraction.from_float(f=4.2)
_ = Fraction.from_decimal(dec=4)

View File

@ -149,10 +149,9 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) {
// Check if we should suppress the fix due to type validation concerns // Check if we should suppress the fix due to type validation concerns
let is_type_safe = is_valid_argument_type(arg_value, method_name, constructor, checker); let is_type_safe = is_valid_argument_type(arg_value, method_name, constructor, checker);
let has_keywords = !call.arguments.keywords.is_empty();
// Determine fix safety // Determine fix safety
let applicability = if is_type_safe && !has_keywords { let applicability = if is_type_safe {
Applicability::Safe Applicability::Safe
} else { } else {
Applicability::Unsafe Applicability::Unsafe
@ -210,21 +209,27 @@ fn is_valid_argument_type(
_ => false, _ => false,
}, },
// Fraction.from_decimal accepts int, bool, Decimal // Fraction.from_decimal accepts int, bool, Decimal
(MethodName::FromDecimal, Constructor::Fraction) => match resolved_type { (MethodName::FromDecimal, Constructor::Fraction) => {
ResolvedPythonType::Atom(PythonType::Number( // First check if it's a Decimal constructor call
NumberLike::Integer | NumberLike::Bool, let is_decimal_call = arg_expr
)) => true, .as_call_expr()
ResolvedPythonType::Unknown => is_int, .and_then(|call| semantic.resolve_qualified_name(&call.func))
_ => { .is_some_and(|qualified_name| {
// Check if it's a Decimal instance matches!(qualified_name.segments(), ["decimal", "Decimal"])
arg_expr });
.as_call_expr()
.and_then(|call| semantic.resolve_qualified_name(&call.func)) if is_decimal_call {
.is_some_and(|qualified_name| { return true;
matches!(qualified_name.segments(), ["decimal", "Decimal"])
})
} }
},
match resolved_type {
ResolvedPythonType::Atom(PythonType::Number(
NumberLike::Integer | NumberLike::Bool,
)) => true,
ResolvedPythonType::Unknown => is_int,
_ => false,
}
}
_ => false, _ => false,
} }
} }
@ -274,7 +279,7 @@ fn handle_non_finite_float_special_case(
return None; return None;
} }
let Expr::Call(ast::ExprCall { let Expr::Call(ExprCall {
func, arguments, .. func, arguments, ..
}) = arg_value }) = arg_value
else { else {

View File

@ -99,7 +99,6 @@ help: Replace with `Fraction` constructor
12 | _ = Fraction.from_decimal(Decimal("-4.2")) 12 | _ = Fraction.from_decimal(Decimal("-4.2"))
13 | _ = Fraction.from_decimal(Decimal.from_float(4.2)) 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 | _ = Decimal.from_float(0.1) 14 | _ = Decimal.from_float(0.1)
note: This is an unsafe fix and may change runtime behavior
FURB164 [*] Verbose method `from_decimal` in `Fraction` construction FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
--> FURB164.py:12:5 --> FURB164.py:12:5
@ -120,7 +119,6 @@ help: Replace with `Fraction` constructor
13 | _ = Fraction.from_decimal(Decimal.from_float(4.2)) 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 | _ = Decimal.from_float(0.1) 14 | _ = Decimal.from_float(0.1)
15 | _ = Decimal.from_float(-0.5) 15 | _ = Decimal.from_float(-0.5)
note: This is an unsafe fix and may change runtime behavior
FURB164 [*] Verbose method `from_decimal` in `Fraction` construction FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
--> FURB164.py:13:5 --> FURB164.py:13:5
@ -484,7 +482,6 @@ help: Replace with `Fraction` constructor
32 | _ = Decimal.from_float(f=4.2) 32 | _ = Decimal.from_float(f=4.2)
33 | 33 |
34 | # Cases with invalid argument counts - should not get fixes 34 | # Cases with invalid argument counts - should not get fixes
note: This is an unsafe fix and may change runtime behavior
FURB164 Verbose method `from_float` in `Decimal` construction FURB164 Verbose method `from_float` in `Decimal` construction
--> FURB164.py:32:5 --> FURB164.py:32:5
@ -658,6 +655,7 @@ help: Replace with `Decimal` constructor
64 + _ = Decimal("nan") 64 + _ = Decimal("nan")
65 | _ = Decimal.from_float(float("\x2dnan")) 65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan")) 66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
67 |
FURB164 [*] Verbose method `from_float` in `Decimal` construction FURB164 [*] Verbose method `from_float` in `Decimal` construction
--> FURB164.py:65:5 --> FURB164.py:65:5
@ -675,6 +673,8 @@ help: Replace with `Decimal` constructor
- _ = Decimal.from_float(float("\x2dnan")) - _ = Decimal.from_float(float("\x2dnan"))
65 + _ = Decimal("nan") 65 + _ = Decimal("nan")
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan")) 66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
FURB164 [*] Verbose method `from_float` in `Decimal` construction FURB164 [*] Verbose method `from_float` in `Decimal` construction
--> FURB164.py:66:5 --> FURB164.py:66:5
@ -683,6 +683,8 @@ FURB164 [*] Verbose method `from_float` in `Decimal` construction
65 | _ = Decimal.from_float(float("\x2dnan")) 65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan")) 66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
| |
help: Replace with `Decimal` constructor help: Replace with `Decimal` constructor
63 | # Cases with non-finite floats - should produce safe fixes 63 | # Cases with non-finite floats - should produce safe fixes
@ -690,3 +692,38 @@ help: Replace with `Decimal` constructor
65 | _ = Decimal.from_float(float("\x2dnan")) 65 | _ = Decimal.from_float(float("\x2dnan"))
- _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan")) - _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
66 + _ = Decimal("nan") 66 + _ = Decimal("nan")
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
FURB164 [*] Verbose method `from_float` in `Fraction` construction
--> FURB164.py:70:5
|
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
70 | _ = Fraction.from_float(f=4.2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
71 | _ = Fraction.from_decimal(dec=4)
|
help: Replace with `Fraction` constructor
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
- _ = Fraction.from_float(f=4.2)
70 + _ = Fraction(4.2)
71 | _ = Fraction.from_decimal(dec=4)
FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
--> FURB164.py:71:5
|
69 | # fixes must be safe
70 | _ = Fraction.from_float(f=4.2)
71 | _ = Fraction.from_decimal(dec=4)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `Fraction` constructor
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
70 | _ = Fraction.from_float(f=4.2)
- _ = Fraction.from_decimal(dec=4)
71 + _ = Fraction(4)