Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid `float('NaN')`
# Rule request

## Thesis

Forbid `float('NaN')` usage. It's not about forbidding `NaN` at all because it can't be detected without a proper type checking. `math.nan` is still allowed.

## Reasoning

NaN is really confusing and is a good way to catch a lot of unexpected bugs:

```
>>> sorted([5.0, math.nan, 10.0, 0.0])
[5.0, nan, 0.0, 10.0]

>>> 3 < math.nan
False
>>> 3 > math.nan
False

# no commutativity
>>> min(3, math.nan)
3
>>> min(math.nan, 3)
nan
```

Even if you're 100% sure what you're doing, use `math.nan` instead. Forbidding `nan` in any form (maybe, even including numpy) can be done later and is outside of this rule request.



Discussion:
Is `float('nan')` and `math.nan` the same thing?
> Is float('nan') and math.nan the same thing?

I guess so

```
>>> type(math.nan)
float
```
Can I take this?
Yes, please! 
So, as far as I understand, I need to flag all and any appearances of `float("NaN")`?
Yes! Keep in mind that "NaN" string can be in any case ("nan", "NAN" etc.).

Resolving patch:
diff --git a/wemake_python_styleguide/presets/types/tree.py b/wemake_python_styleguide/presets/types/tree.py
--- a/wemake_python_styleguide/presets/types/tree.py
+++ b/wemake_python_styleguide/presets/types/tree.py
@@ -54,6 +54,7 @@
     functions.WrongFunctionCallContextVisitior,
     functions.UnnecessaryLiteralsVisitor,
     functions.PositionalOnlyArgumentsVisitor,
+    functions.FloatingNanCallVisitor,
 
     exceptions.WrongTryExceptVisitor,
     exceptions.NestedTryBlocksVisitor,
diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py
--- a/wemake_python_styleguide/violations/best_practices.py
+++ b/wemake_python_styleguide/violations/best_practices.py
@@ -72,6 +72,7 @@
    ShebangViolation
    BaseExceptionRaiseViolation
    NonTrivialExceptViolation
+   FloatingNanViolation
 
 Best practices
 --------------
@@ -132,6 +133,7 @@
 .. autoclass:: ShebangViolation
 .. autoclass:: BaseExceptionRaiseViolation
 .. autoclass:: NonTrivialExceptViolation
+.. autoclass:: FloatingNanViolation
 
 """
 
@@ -2194,3 +2196,31 @@ class NonTrivialExceptViolation(ASTViolation):
 
     error_template = 'Found non-trivial expression as an argument for "except"'
     code = 455
+
+
+@final
+class FloatingNanViolation(ASTViolation):
+    """
+    Forbids to use ``float("NaN")`` construct to generate NaN.
+
+    Reasoning:
+        This method to generate NaN is really confusing and is a good way to
+        catch a lot of unexpected bugs.
+
+    Solution:
+        Even if you're 100% sure what you're doing, use ``math.nan`` instead.
+
+    Example::
+
+        # Correct:
+        min(math.nan, 3)
+
+        # Wrong:
+        min(float("NAN"), 3)
+
+    .. versionadded:: 0.16.0
+
+    """
+
+    error_template = 'Found "NaN" as argument to float()'
+    code = 456
diff --git a/wemake_python_styleguide/visitors/ast/functions.py b/wemake_python_styleguide/visitors/ast/functions.py
--- a/wemake_python_styleguide/visitors/ast/functions.py
+++ b/wemake_python_styleguide/visitors/ast/functions.py
@@ -28,6 +28,7 @@
 from wemake_python_styleguide.violations.best_practices import (
     BooleanPositionalArgumentViolation,
     ComplexDefaultValueViolation,
+    FloatingNanViolation,
     PositionalOnlyArgumentsViolation,
     StopIterationInsideGeneratorViolation,
     WrongFunctionCallViolation,
@@ -172,6 +173,31 @@ def _is_call_ignored(self, node: ast.Call) -> bool:
         )
 
 
+@final
+class FloatingNanCallVisitor(base.BaseNodeVisitor):
+    """Ensure that NaN is acquired in an explicit way."""
+
+    _nan_variants = frozenset(('nan', b'nan'))
+
+    def visit_Call(self, node: ast.Call) -> None:
+        """Used to find ``float("NaN")`` calls."""
+        self._check_floating_nan(node)
+        self.generic_visit(node)
+
+    def _check_floating_nan(self, node: ast.Call) -> None:
+        if len(node.args) != 1:
+            return
+
+        if not isinstance(node.args[0], (ast.Str, ast.Bytes)):
+            return
+
+        if not functions.given_function_called(node, 'float'):
+            return
+
+        if node.args[0].s.lower() in self._nan_variants:
+            self.add_violation(FloatingNanViolation(node))
+
+
 @final
 class WrongFunctionCallContextVisitior(base.BaseNodeVisitor):
     """Ensure that we call several functions in the correct context."""


Test patch:
diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py
--- a/tests/fixtures/noqa/noqa.py
+++ b/tests/fixtures/noqa/noqa.py
@@ -722,3 +722,5 @@ def raise_bad_exception():
 except ValueError or TypeError:  # noqa: WPS455
     my_print("Oops.")
 
+if float("NaN") < number:  # noqa: WPS456
+    my_print("Greater than... what?")
diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py
--- a/tests/test_checker/test_noqa.py
+++ b/tests/test_checker/test_noqa.py
@@ -224,6 +224,7 @@
     'WPS453': 0,
     'WPS454': 1,
     'WPS455': 1,
+    'WPS456': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_functions/test_floating_nan.py b/tests/test_visitors/test_ast/test_functions/test_floating_nan.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_functions/test_floating_nan.py
@@ -0,0 +1,70 @@
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    FloatingNanViolation,
+)
+from wemake_python_styleguide.visitors.ast.functions import (
+    FloatingNanCallVisitor,
+)
+
+
+@pytest.mark.parametrize('code', [
+    'float(4.5) + 4.2',
+    'float(2) + 0',
+    'result = "NaN".join(["a", "a"])',
+    'a = math.nan',
+    'my_function("NaN")',
+    'float("123") + math.nan',
+])
+def test_correct_nan_acquisition(
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+    code,
+    mode,
+):
+    """Testing that acquiring NaN in a proper way is not  as a violation."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = FloatingNanCallVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('prefix', [
+    'b',
+    'u',
+    '',
+])
+@pytest.mark.parametrize('nan_variant', [
+    'NaN',
+    'NAN',
+    'nan',
+    'Nan',
+    'nAn',
+    'nAN',
+    'naN',
+    'NAn',
+])
+@pytest.mark.parametrize('code', [
+    'float({0})',
+    'float({0}) < 0.0',
+    'sorted([5.0, float({0}), 10.0, 0.0])',
+])
+def test_floating_nan(
+    assert_errors,
+    parse_ast_tree,
+    prefix,
+    nan_variant,
+    code,
+    default_options,
+):
+    """Testing that "NaN" argument to ``float()`` is as a violation."""
+    nan = '{0}"{1}"'.format(prefix, nan_variant)
+    tree = parse_ast_tree(code.format(nan))
+
+    visitor = FloatingNanCallVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [FloatingNanViolation])