diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py b/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py index 3481b37d6e..af7cbff7b3 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py @@ -92,3 +92,8 @@ if y == np.inf: # OK if x == "nan": pass + +# PLW0117 +# https://github.com/astral-sh/ruff/issues/18596 +assert x == float("-NaN ") +assert x == float(" \n+nan \t") diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index ffc9d17e99..d5d687f7c4 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -39,6 +39,8 @@ use crate::settings::{LinterSettings, TargetVersion, flags}; use crate::source_kind::SourceKind; use crate::{Locator, directives, fs, warn_user_once}; +pub(crate) mod float; + pub struct LinterResult { /// A collection of diagnostic messages generated by the linter. pub messages: Vec, diff --git a/crates/ruff_linter/src/linter/float.rs b/crates/ruff_linter/src/linter/float.rs new file mode 100644 index 0000000000..a61da05ea4 --- /dev/null +++ b/crates/ruff_linter/src/linter/float.rs @@ -0,0 +1,39 @@ +use ruff_python_ast as ast; + +/// Checks if `expr` is a string literal that represents NaN. +/// E.g., `"NaN"`, `"-nAn"`, `"+nan"`, or even `" -NaN \n \t"` +/// Returns `None` if it's not. Else `Some("nan")`, `Some("-nan")`, or `Some("+nan")`. +pub(crate) fn as_nan_float_string_literal(expr: &ast::Expr) -> Option<&'static str> { + find_any_ignore_ascii_case(expr, &["nan", "+nan", "-nan"]) +} + +/// Returns `true` if `expr` is a string literal that represents a non-finite float. +/// E.g., `"NaN"`, "-inf", `"Infinity"`, or even `" +Inf \n \t"`. +/// Return `None` if it's not. Else the lowercased, trimmed string literal, +/// e.g., `Some("nan")`, `Some("-inf")`, or `Some("+infinity")`. +pub(crate) fn as_non_finite_float_string_literal(expr: &ast::Expr) -> Option<&'static str> { + find_any_ignore_ascii_case( + expr, + &[ + "nan", + "+nan", + "-nan", + "inf", + "+inf", + "-inf", + "infinity", + "+infinity", + "-infinity", + ], + ) +} + +fn find_any_ignore_ascii_case(expr: &ast::Expr, patterns: &[&'static str]) -> Option<&'static str> { + let value = &expr.as_string_literal_expr()?.value; + + let value = value.to_str().trim(); + patterns + .iter() + .find(|other| value.eq_ignore_ascii_case(other)) + .copied() +} diff --git a/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs index 129395affd..267e5e7ec6 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs @@ -1,10 +1,12 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; + use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; +use crate::linter::float::as_nan_float_string_literal; /// ## What it does /// Checks for comparisons against NaN values. @@ -113,14 +115,10 @@ fn is_nan_float(expr: &Expr, semantic: &SemanticModel) -> bool { return false; } - let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = &**args else { + let [expr] = &**args else { return false; }; - - if !matches!( - value.to_str(), - "nan" | "NaN" | "NAN" | "Nan" | "nAn" | "naN" | "nAN" | "NAn" - ) { + if as_nan_float_string_literal(expr).is_none() { return false; } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap index a9c4292f14..3cc8d69737 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap @@ -107,3 +107,20 @@ nan_comparison.py:60:10: PLW0177 Comparing against a NaN value; use `math.isnan` 61 | 62 | # No errors | + +nan_comparison.py:98:13: PLW0177 Comparing against a NaN value; use `math.isnan` instead + | +96 | # PLW0117 +97 | # https://github.com/astral-sh/ruff/issues/18596 +98 | assert x == float("-NaN ") + | ^^^^^^^^^^^^^^ PLW0177 +99 | assert x == float(" \n+nan \t") + | + +nan_comparison.py:99:13: PLW0177 Comparing against a NaN value; use `math.isnan` instead + | +97 | # https://github.com/astral-sh/ruff/issues/18596 +98 | assert x == float("-NaN ") +99 | assert x == float(" \n+nan \t") + | ^^^^^^^^^^^^^^^^^^^^^ PLW0177 + | diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs index 8303dd4903..0aa17845ae 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs @@ -140,6 +140,7 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { let Some(float) = float.as_string_literal_expr() else { break 'short_circuit; }; + // FIXME: use `as_non_finite_float_string_literal` instead. if !matches!( float.value.to_str().to_lowercase().as_str(), "inf" | "-inf" | "infinity" | "-infinity" | "nan" diff --git a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs index b2d3b12b9e..a08489d2d1 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs @@ -1,10 +1,12 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; + use ruff_python_ast::{self as ast, Expr}; use ruff_python_trivia::PythonWhitespace; use ruff_text_size::Ranged; use std::borrow::Cow; use crate::checkers::ast::Checker; +use crate::linter::float::as_non_finite_float_string_literal; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does @@ -150,35 +152,13 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal let [float] = arguments.args.as_ref() else { return; }; - let Some(float) = float.as_string_literal_expr() else { + let Some(float_str) = as_non_finite_float_string_literal(float) else { return; }; - let trimmed = float.value.to_str().trim(); - let mut matches_non_finite_keyword = false; - for non_finite_keyword in [ - "inf", - "+inf", - "-inf", - "infinity", - "+infinity", - "-infinity", - "nan", - "+nan", - "-nan", - ] { - if trimmed.eq_ignore_ascii_case(non_finite_keyword) { - matches_non_finite_keyword = true; - break; - } - } - if !matches_non_finite_keyword { - return; - } - let mut replacement = checker.locator().slice(float).to_string(); // `Decimal(float("-nan")) == Decimal("nan")` - if trimmed.eq_ignore_ascii_case("-nan") { + if float_str == "-nan" { // Here we do not attempt to remove just the '-' character. // It may have been encoded (e.g. as '\N{hyphen-minus}') // in the original source slice, and the added complexity