diff options
author | Or Bahari <orbahari@mail.tau.ac.il> | 2021-02-02 01:44:46 +0200 |
---|---|---|
committer | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2021-02-18 15:22:07 +0100 |
commit | f9b2227cb036349dda36101d904496e2b54167e8 (patch) | |
tree | e24a3871bdb2ea412f16fa3010a99be3468d7cab | |
parent | 4faccd0563c628954f04322ffb696905a3dab62d (diff) | |
download | pylint-git-f9b2227cb036349dda36101d904496e2b54167e8.tar.gz |
add nan-comparison checker for NaN comparisons
**After fix whatsnew to 2.7
-rw-r--r-- | ChangeLog | 2 | ||||
-rw-r--r-- | doc/whatsnew/2.7.rst | 2 | ||||
-rw-r--r-- | pylint/checkers/base.py | 59 | ||||
-rw-r--r-- | tests/checkers/unittest_base.py | 37 | ||||
-rw-r--r-- | tests/functional/n/nan_comparison_check.py | 21 | ||||
-rw-r--r-- | tests/functional/n/nan_comparison_check.txt | 12 |
6 files changed, 131 insertions, 2 deletions
@@ -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)' |