Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,8 @@
# OK
if x == "nan":
pass

# PLW0117
# https://github.com/astral-sh/ruff/issues/18596
assert x == float("-NaN ")
assert x == float(" \n+nan \t")
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Message>,
Expand Down
39 changes: 39 additions & 0 deletions crates/ruff_linter/src/linter/float.rs
Original file line number Diff line number Diff line change
@@ -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()
}
10 changes: 4 additions & 6 deletions crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading