[pylint] Detect more exotic NaN literals in PLW0177 (#18630)
Some checks are pending
CI / cargo test (linux) (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Robsdedude 2025-06-19 11:05:06 +00:00 committed by GitHub
parent 136443b71b
commit 4e83db4d40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 72 additions and 30 deletions

View file

@ -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")

View file

@ -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>,

View file

@ -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()
}

View file

@ -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;
}

View file

@ -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
|

View file

@ -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"

View file

@ -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