summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOr Bahari <orbahari@mail.tau.ac.il>2021-02-02 01:44:46 +0200
committerPierre Sassoulas <pierre.sassoulas@gmail.com>2021-02-18 15:22:07 +0100
commitf9b2227cb036349dda36101d904496e2b54167e8 (patch)
treee24a3871bdb2ea412f16fa3010a99be3468d7cab
parent4faccd0563c628954f04322ffb696905a3dab62d (diff)
downloadpylint-git-f9b2227cb036349dda36101d904496e2b54167e8.tar.gz
add nan-comparison checker for NaN comparisons
**After fix whatsnew to 2.7
-rw-r--r--ChangeLog2
-rw-r--r--doc/whatsnew/2.7.rst2
-rw-r--r--pylint/checkers/base.py59
-rw-r--r--tests/checkers/unittest_base.py37
-rw-r--r--tests/functional/n/nan_comparison_check.py21
-rw-r--r--tests/functional/n/nan_comparison_check.txt12
6 files changed, 131 insertions, 2 deletions
diff --git a/ChangeLog b/ChangeLog
index 7a7d6f4e0..f3badb564 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -5,6 +5,8 @@ Pylint's ChangeLog
What's New in Pylint 2.7.0?
===========================
+* Add `nan-comparison` check for NaN comparisons
+
* Python 3.6+ is now required.
* Bug fix for empty-comment message line number.
diff --git a/doc/whatsnew/2.7.rst b/doc/whatsnew/2.7.rst
index 0ee5531f5..1f7bab85a 100644
--- a/doc/whatsnew/2.7.rst
+++ b/doc/whatsnew/2.7.rst
@@ -15,6 +15,8 @@ Summary -- Release highlights
New checkers
============
+* Add `nan-comparison` check for comparison of NaN values
+
* Add support to ``ignored-argument-names`` in DocstringParameterChecker and
adds `useless-param-doc` and `useless-type-doc` messages.
diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py
index aeaa85379..cc7e90237 100644
--- a/pylint/checkers/base.py
+++ b/pylint/checkers/base.py
@@ -1661,7 +1661,6 @@ KNOWN_NAME_TYPES = {
"inlinevar",
}
-
HUMAN_READABLE_TYPES = {
"module": "module",
"const": "constant",
@@ -1724,7 +1723,6 @@ def _create_naming_options():
class NameChecker(_BasicChecker):
-
msgs = {
"C0102": (
'Black listed name "%s"',
@@ -2342,6 +2340,12 @@ class ComparisonChecker(_BasicChecker):
"callable was made, which might suggest that some parenthesis were omitted, "
"resulting in potential unwanted behaviour.",
),
+ "W0177": (
+ "Comparison %s should be %s",
+ "nan-comparison",
+ "Used when an expression is compared to NaN"
+ "values like numpy.NaN and float('nan')",
+ ),
}
def _check_singleton_comparison(
@@ -2402,6 +2406,52 @@ class ComparisonChecker(_BasicChecker):
args=(f"'{root_node.as_string()}'", suggestion),
)
+ def _check_nan_comparison(
+ self, left_value, right_value, root_node, checking_for_absence: bool = False
+ ):
+ def _is_float_nan(node):
+ try:
+ if isinstance(node, astroid.Call) and len(node.args) == 1:
+ if (
+ node.args[0].value.lower() == "nan"
+ and node.inferred()[0].pytype() == "builtins.float"
+ ):
+ return True
+ return False
+ except AttributeError:
+ return False
+
+ def _is_numpy_nan(node):
+ if isinstance(node, astroid.Attribute) and node.attrname == "NaN":
+ if isinstance(node.expr, astroid.Name):
+ return node.expr.name in ("numpy", "nmp", "np")
+ return False
+
+ def _is_nan(node) -> bool:
+ return _is_float_nan(node) or _is_numpy_nan(node)
+
+ nan_left = _is_nan(left_value)
+ if not nan_left and not _is_nan(right_value):
+ return
+
+ absence_text = ""
+ if checking_for_absence:
+ absence_text = "not "
+ if nan_left:
+ suggestion = "'{}math.isnan({})'".format(
+ absence_text, right_value.as_string()
+ )
+ else:
+ suggestion = "'{}math.isnan({})'".format(
+ absence_text, left_value.as_string()
+ )
+
+ self.add_message(
+ "nan-comparison",
+ node=root_node,
+ args=("'{}'".format(root_node.as_string()), suggestion),
+ )
+
def _check_literal_comparison(self, literal, node):
"""Check if we compare to a literal, which is usually what we do not want to do."""
nodes = (astroid.List, astroid.Tuple, astroid.Dict, astroid.Set)
@@ -2495,6 +2545,11 @@ class ComparisonChecker(_BasicChecker):
self._check_singleton_comparison(
left, right, node, checking_for_absence=operator == "!="
)
+
+ if operator in ("==", "!=", "is", "is not"):
+ self._check_nan_comparison(
+ left, right, node, checking_for_absence=operator in ("!=", "is not")
+ )
if operator in ("is", "is not"):
self._check_literal_comparison(right, node)
diff --git a/tests/checkers/unittest_base.py b/tests/checkers/unittest_base.py
index a106e8c2b..3c2e443dc 100644
--- a/tests/checkers/unittest_base.py
+++ b/tests/checkers/unittest_base.py
@@ -445,6 +445,43 @@ class TestComparison(CheckerTestCase):
with self.assertAddsMessages(message):
self.checker.visit_compare(node)
+ node = astroid.extract_node("foo is float('nan')")
+ message = Message(
+ "nan-comparison",
+ node=node,
+ args=("'foo is float('nan')'", "'math.isnan(foo)'"),
+ )
+ with self.assertAddsMessages(message):
+ self.checker.visit_compare(node)
+
+ node = astroid.extract_node(
+ """
+ import numpy
+ foo != numpy.NaN
+ """
+ )
+ message = Message(
+ "nan-comparison",
+ node=node,
+ args=("'foo != numpy.NaN'", "'not math.isnan(foo)'"),
+ )
+ with self.assertAddsMessages(message):
+ self.checker.visit_compare(node)
+
+ node = astroid.extract_node(
+ """
+ import numpy as nmp
+ foo is not nmp.NaN
+ """
+ )
+ message = Message(
+ "nan-comparison",
+ node=node,
+ args=("'foo is not nmp.NaN'", "'not math.isnan(foo)'"),
+ )
+ with self.assertAddsMessages(message):
+ self.checker.visit_compare(node)
+
node = astroid.extract_node("True == foo")
messages = (
Message("misplaced-comparison-constant", node=node, args=("foo == True",)),
diff --git a/tests/functional/n/nan_comparison_check.py b/tests/functional/n/nan_comparison_check.py
new file mode 100644
index 000000000..db539a11e
--- /dev/null
+++ b/tests/functional/n/nan_comparison_check.py
@@ -0,0 +1,21 @@
+# pylint: disable=missing-docstring, invalid-name, misplaced-comparison-constant,literal-comparison,comparison-with-itself, import-error
+"""Test detection of NaN value comparison."""
+
+import numpy
+x = 42
+a = x is numpy.NaN # [nan-comparison]
+b = x == numpy.NaN # [nan-comparison]
+c = x == float('nan') # [nan-comparison]
+e = numpy.NaN == numpy.NaN # [nan-comparison]
+f = x is 1
+g = 123 is "123"
+h = numpy.NaN is not x # [nan-comparison]
+i = numpy.NaN != x # [nan-comparison]
+
+j = x != numpy.NaN # [nan-comparison]
+j1 = x != float('nan') # [nan-comparison]
+assert x == numpy.NaN # [nan-comparison]
+assert x is not float('nan') # [nan-comparison]
+if x == numpy.NaN: # [nan-comparison]
+ pass
+z = bool(x is numpy.NaN) # [nan-comparison]
diff --git a/tests/functional/n/nan_comparison_check.txt b/tests/functional/n/nan_comparison_check.txt
new file mode 100644
index 000000000..2351ea403
--- /dev/null
+++ b/tests/functional/n/nan_comparison_check.txt
@@ -0,0 +1,12 @@
+nan-comparison:6:4::Comparison 'x is numpy.NaN' should be 'math.isnan(x)'
+nan-comparison:7:4::Comparison 'x == numpy.NaN' should be 'math.isnan(x)'
+nan-comparison:8:4::Comparison 'x == float('nan')' should be 'math.isnan(x)'
+nan-comparison:9:4::Comparison 'numpy.NaN == numpy.NaN' should be 'math.isnan(numpy.NaN)'
+nan-comparison:12:4::Comparison 'numpy.NaN is not x' should be 'not math.isnan(x)'
+nan-comparison:13:4::Comparison 'numpy.NaN != x' should be 'not math.isnan(x)'
+nan-comparison:15:4::Comparison 'x != numpy.NaN' should be 'not math.isnan(x)'
+nan-comparison:16:5::Comparison 'x != float('nan')' should be 'not math.isnan(x)'
+nan-comparison:17:7::Comparison 'x == numpy.NaN' should be 'math.isnan(x)'
+nan-comparison:18:7::Comparison 'x is not float('nan')' should be 'not math.isnan(x)'
+nan-comparison:19:3::Comparison 'x == numpy.NaN' should be 'math.isnan(x)'
+nan-comparison:21:9::Comparison 'x is numpy.NaN' should be 'math.isnan(x)'